windowcapture
исходный код / Models/CommentBubble.cs

CommentBubble.cs

313 строк · 11,845 байт · модуль Models
  1using System;
  2using System.Drawing;
  3using System.Drawing.Drawing2D;
  4
  5namespace WindowCapture.Models
  6{
  7    public enum BubbleShape
  8    {
  9        RoundedRect,
 10        Oval,
 11        Rectangle
 12    }
 13
 14    public class CommentBubble
 15    {
 16        public Point Location;      // Top-left of bubble
 17        public Point TailPoint;     // Where the tail points to
 18        public string Text = "";
 19        public Font TextFont;
 20        public Color FillColor;
 21        public Color BorderColor;
 22        public Color TextColor;
 23        public int BorderWidth;
 24        public bool IsEditing;
 25        public int Padding = 10;
 26        public int CornerRadius = 8;
 27        public int TailWidth = 15;
 28        public int TailHeight = 12;
 29        public BubbleShape Shape = BubbleShape.RoundedRect;
 30
 31        public CommentBubble(Point location)
 32        {
 33            Location = location;
 34            TailPoint = new Point(location.X + 20, location.Y + 60);
 35            TextFont = new Font(Settings.BubbleFont, Settings.BubbleFontSize);
 36            FillColor = Settings.BubbleFillColor;
 37            BorderColor = Settings.BubbleBorderColor;
 38            TextColor = Color.Black;
 39            BorderWidth = Settings.BubbleBorderWidth;
 40            IsEditing = true;
 41        }
 42
 43        public Rectangle GetBubbleRect(Graphics g)
 44        {
 45            SizeF textSize;
 46            if (string.IsNullOrEmpty(Text))
 47                textSize = new SizeF(80, 20); // Minimum
 48            else
 49                textSize = g.MeasureString(Text, TextFont);
 50
 51            int w = (int)textSize.Width + Padding * 2;
 52            int h = (int)textSize.Height + Padding * 2;
 53            return new Rectangle(Location.X, Location.Y, w, h);
 54        }
 55
 56        public void CycleShape()
 57        {
 58            switch (Shape)
 59            {
 60                case BubbleShape.RoundedRect:
 61                    Shape = BubbleShape.Oval;
 62                    break;
 63                case BubbleShape.Oval:
 64                    Shape = BubbleShape.Rectangle;
 65                    break;
 66                case BubbleShape.Rectangle:
 67                    Shape = BubbleShape.RoundedRect;
 68                    break;
 69            }
 70        }
 71
 72        public GraphicsPath GetBubblePath(Graphics g)
 73        {
 74            var rect = GetBubbleRect(g);
 75            var path = new GraphicsPath();
 76
 77            // Determine tail attachment point on bubble edge
 78            Point tailAttach = GetTailAttachPoint(rect);
 79
 80            switch (Shape)
 81            {
 82                case BubbleShape.Oval:
 83                    DrawOvalWithTail(path, rect, tailAttach);
 84                    break;
 85                case BubbleShape.Rectangle:
 86                    DrawRectWithTail(path, rect, tailAttach);
 87                    break;
 88                default: // RoundedRect
 89                    DrawRoundedRectWithTail(path, rect, tailAttach);
 90                    break;
 91            }
 92
 93            return path;
 94        }
 95
 96        private Point GetTailAttachPoint(Rectangle rect)
 97        {
 98            // Calculate which edge the tail should attach to based on TailPoint position
 99            int cx = rect.X + rect.Width / 2;
100            int cy = rect.Y + rect.Height / 2;
101
102            // Default: attach to bottom
103            int attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X));
104            int attachY = rect.Bottom;
105
106            // Check if tail points to sides or top
107            float dx = TailPoint.X - cx;
108            float dy = TailPoint.Y - cy;
109
110            if (Math.Abs(dx) > Math.Abs(dy))
111            {
112                // Horizontal - left or right
113                if (dx < 0)
114                {
115                    attachX = rect.X;
116                    attachY = Math.Max(rect.Y + TailWidth, Math.Min(rect.Bottom - TailWidth, TailPoint.Y));
117                }
118                else
119                {
120                    attachX = rect.Right;
121                    attachY = Math.Max(rect.Y + TailWidth, Math.Min(rect.Bottom - TailWidth, TailPoint.Y));
122                }
123            }
124            else
125            {
126                // Vertical - top or bottom
127                if (dy < 0)
128                {
129                    attachY = rect.Y;
130                    attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X));
131                }
132                else
133                {
134                    attachY = rect.Bottom;
135                    attachX = Math.Max(rect.X + TailWidth, Math.Min(rect.Right - TailWidth, TailPoint.X));
136                }
137            }
138
139            return new Point(attachX, attachY);
140        }
141
142        private void DrawRoundedRectWithTail(GraphicsPath path, Rectangle rect, Point tailAttach)
143        {
144            int r = CornerRadius;
145            bool tailOnBottom = tailAttach.Y == rect.Bottom;
146            bool tailOnTop = tailAttach.Y == rect.Y;
147            bool tailOnLeft = tailAttach.X == rect.X;
148            bool tailOnRight = tailAttach.X == rect.Right;
149
150            // Top-left corner
151            path.AddArc(rect.X, rect.Y, r * 2, r * 2, 180, 90);
152
153            // Top edge with tail
154            if (tailOnTop)
155            {
156                path.AddLine(rect.X + r, rect.Y, tailAttach.X - TailWidth / 2, rect.Y);
157                path.AddLine(tailAttach.X - TailWidth / 2, rect.Y, TailPoint.X, TailPoint.Y);
158                path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X + TailWidth / 2, rect.Y);
159                path.AddLine(tailAttach.X + TailWidth / 2, rect.Y, rect.Right - r, rect.Y);
160            }
161
162            // Top-right corner
163            path.AddArc(rect.Right - r * 2, rect.Y, r * 2, r * 2, 270, 90);
164
165            // Right edge with tail
166            if (tailOnRight)
167            {
168                path.AddLine(rect.Right, rect.Y + r, rect.Right, tailAttach.Y - TailWidth / 2);
169                path.AddLine(rect.Right, tailAttach.Y - TailWidth / 2, TailPoint.X, TailPoint.Y);
170                path.AddLine(TailPoint.X, TailPoint.Y, rect.Right, tailAttach.Y + TailWidth / 2);
171                path.AddLine(rect.Right, tailAttach.Y + TailWidth / 2, rect.Right, rect.Bottom - r);
172            }
173
174            // Bottom-right corner
175            path.AddArc(rect.Right - r * 2, rect.Bottom - r * 2, r * 2, r * 2, 0, 90);
176
177            // Bottom edge with tail
178            if (tailOnBottom)
179            {
180                path.AddLine(rect.Right - r, rect.Bottom, tailAttach.X + TailWidth / 2, rect.Bottom);
181                path.AddLine(tailAttach.X + TailWidth / 2, rect.Bottom, TailPoint.X, TailPoint.Y);
182                path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X - TailWidth / 2, rect.Bottom);
183                path.AddLine(tailAttach.X - TailWidth / 2, rect.Bottom, rect.X + r, rect.Bottom);
184            }
185
186            // Bottom-left corner
187            path.AddArc(rect.X, rect.Bottom - r * 2, r * 2, r * 2, 90, 90);
188
189            // Left edge with tail
190            if (tailOnLeft)
191            {
192                path.AddLine(rect.X, rect.Bottom - r, rect.X, tailAttach.Y + TailWidth / 2);
193                path.AddLine(rect.X, tailAttach.Y + TailWidth / 2, TailPoint.X, TailPoint.Y);
194                path.AddLine(TailPoint.X, TailPoint.Y, rect.X, tailAttach.Y - TailWidth / 2);
195                path.AddLine(rect.X, tailAttach.Y - TailWidth / 2, rect.X, rect.Y + r);
196            }
197
198            path.CloseFigure();
199        }
200
201        private void DrawOvalWithTail(GraphicsPath path, Rectangle rect, Point tailAttach)
202        {
203            // Draw ellipse with tail
204            path.AddEllipse(rect);
205
206            // Add tail as separate triangle
207            var tailPath = new GraphicsPath();
208            int tw = TailWidth / 2;
209
210            // Calculate points on ellipse edge near tail attach
211            float cx = rect.X + rect.Width / 2f;
212            float cy = rect.Y + rect.Height / 2f;
213            float angle = (float)Math.Atan2(TailPoint.Y - cy, TailPoint.X - cx);
214
215            float rx = rect.Width / 2f;
216            float ry = rect.Height / 2f;
217
218            float x1 = cx + rx * (float)Math.Cos(angle - 0.3f);
219            float y1 = cy + ry * (float)Math.Sin(angle - 0.3f);
220            float x2 = cx + rx * (float)Math.Cos(angle + 0.3f);
221            float y2 = cy + ry * (float)Math.Sin(angle + 0.3f);
222
223            path.AddPolygon(new PointF[] {
224                new PointF(x1, y1),
225                new PointF(TailPoint.X, TailPoint.Y),
226                new PointF(x2, y2)
227            });
228        }
229
230        private void DrawRectWithTail(GraphicsPath path, Rectangle rect, Point tailAttach)
231        {
232            bool tailOnBottom = tailAttach.Y == rect.Bottom;
233            bool tailOnTop = tailAttach.Y == rect.Y;
234            bool tailOnLeft = tailAttach.X == rect.X;
235            bool tailOnRight = tailAttach.X == rect.Right;
236
237            // Start from top-left
238            path.AddLine(rect.X, rect.Y, rect.Right, rect.Y);
239
240            // Top edge with tail
241            if (tailOnTop)
242            {
243                path.Reset();
244                path.AddLine(rect.X, rect.Y, tailAttach.X - TailWidth / 2, rect.Y);
245                path.AddLine(tailAttach.X - TailWidth / 2, rect.Y, TailPoint.X, TailPoint.Y);
246                path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X + TailWidth / 2, rect.Y);
247                path.AddLine(tailAttach.X + TailWidth / 2, rect.Y, rect.Right, rect.Y);
248            }
249
250            // Right edge
251            if (tailOnRight)
252            {
253                path.AddLine(rect.Right, rect.Y, rect.Right, tailAttach.Y - TailWidth / 2);
254                path.AddLine(rect.Right, tailAttach.Y - TailWidth / 2, TailPoint.X, TailPoint.Y);
255                path.AddLine(TailPoint.X, TailPoint.Y, rect.Right, tailAttach.Y + TailWidth / 2);
256                path.AddLine(rect.Right, tailAttach.Y + TailWidth / 2, rect.Right, rect.Bottom);
257            }
258            else
259            {
260                path.AddLine(rect.Right, rect.Y, rect.Right, rect.Bottom);
261            }
262
263            // Bottom edge
264            if (tailOnBottom)
265            {
266                path.AddLine(rect.Right, rect.Bottom, tailAttach.X + TailWidth / 2, rect.Bottom);
267                path.AddLine(tailAttach.X + TailWidth / 2, rect.Bottom, TailPoint.X, TailPoint.Y);
268                path.AddLine(TailPoint.X, TailPoint.Y, tailAttach.X - TailWidth / 2, rect.Bottom);
269                path.AddLine(tailAttach.X - TailWidth / 2, rect.Bottom, rect.X, rect.Bottom);
270            }
271            else
272            {
273                path.AddLine(rect.Right, rect.Bottom, rect.X, rect.Bottom);
274            }
275
276            // Left edge
277            if (tailOnLeft)
278            {
279                path.AddLine(rect.X, rect.Bottom, rect.X, tailAttach.Y + TailWidth / 2);
280                path.AddLine(rect.X, tailAttach.Y + TailWidth / 2, TailPoint.X, TailPoint.Y);
281                path.AddLine(TailPoint.X, TailPoint.Y, rect.X, tailAttach.Y - TailWidth / 2);
282                path.AddLine(rect.X, tailAttach.Y - TailWidth / 2, rect.X, rect.Y);
283            }
284            else
285            {
286                path.AddLine(rect.X, rect.Bottom, rect.X, rect.Y);
287            }
288
289            path.CloseFigure();
290        }
291
292        public bool Contains(Point pt, Graphics g)
293        {
294            using (var path = GetBubblePath(g))
295            {
296                return path.IsVisible(pt);
297            }
298        }
299
300        public Rectangle GetTailHandleRect()
301        {
302            int handleSize = 8;
303            return new Rectangle(TailPoint.X - handleSize / 2, TailPoint.Y - handleSize / 2, handleSize, handleSize);
304        }
305
306        public Rectangle GetTextBounds(Graphics g)
307        {
308            var bubbleRect = GetBubbleRect(g);
309            return new Rectangle(bubbleRect.X + Padding, bubbleRect.Y + Padding,
310                                 bubbleRect.Width - Padding * 2, bubbleRect.Height - Padding * 2);
311        }
312    }
313}