1+ using System ;
2+ using System . ComponentModel ;
3+ using System . Drawing ;
4+ using System . Drawing . Drawing2D ;
5+ using System . Drawing . Imaging ;
6+ using System . Windows . Forms ;
7+ using Svg ;
8+
9+ namespace BluePointLilac . Controls
10+ {
11+ public class RComboBox : ComboBox
12+ {
13+ private Color borderColor = Color . FromArgb ( 210 , 210 , 215 ) ;
14+ private Color hoverBorderColor = Color . FromArgb ( 255 , 145 , 60 ) ;
15+ private Color focusBorderColor = Color . FromArgb ( 255 , 107 , 0 ) ;
16+ private Color backgroundColor = Color . FromArgb ( 250 , 250 , 252 ) ;
17+ private Color arrowColor = Color . FromArgb ( 100 , 100 , 100 ) ;
18+ private Color hoverArrowColor = Color . FromArgb ( 255 , 107 , 0 ) ;
19+ private Color buttonColor = Color . FromArgb ( 250 , 250 , 252 ) ;
20+
21+ private bool isHovered = false ;
22+ private bool isFocused = false ;
23+ private int borderRadius = 8 ;
24+
25+ private Bitmap cachedArrowBitmap ;
26+ private Color lastArrowColor = Color . Empty ;
27+ private Rectangle arrowRect ;
28+
29+ private Timer animationTimer ;
30+ private float currentBorderWidth = 1.2f ;
31+ private float targetBorderWidth = 1.2f ;
32+ private Color currentBorderColor ;
33+ private Color targetBorderColor ;
34+ private float arrowScale = 1.0f ;
35+ private float targetArrowScale = 1.0f ;
36+
37+ private bool isPainting = false ;
38+
39+ public RComboBox ( )
40+ {
41+ SetStyle ( ControlStyles . ResizeRedraw | ControlStyles . OptimizedDoubleBuffer |
42+ ControlStyles . AllPaintingInWmPaint | ControlStyles . UserPaint , true ) ;
43+
44+ DrawMode = DrawMode . OwnerDrawFixed ;
45+ DropDownStyle = ComboBoxStyle . DropDownList ;
46+ IntegralHeight = false ; // 防止下拉菜单高度问题
47+
48+ BackColor = MyMainForm . ButtonMain ;
49+ ForeColor = MyMainForm . FormFore ;
50+
51+ InitializeColors ( ) ;
52+ InitializeAnimation ( ) ;
53+ UpdateArrowRect ( ) ;
54+
55+ // 事件处理
56+ MouseEnter += ( s , e ) => { isHovered = true ; UpdateAnimationTargets ( ) ; Invalidate ( ) ; } ;
57+ MouseLeave += ( s , e ) => { isHovered = false ; UpdateAnimationTargets ( ) ; Invalidate ( ) ; } ;
58+ GotFocus += ( s , e ) => { isFocused = true ; UpdateAnimationTargets ( ) ; Invalidate ( ) ; } ;
59+ LostFocus += ( s , e ) => { isFocused = false ; UpdateAnimationTargets ( ) ; Invalidate ( ) ; } ;
60+
61+ // 修复下拉菜单显示问题
62+ DropDown += OnDropDown ;
63+ }
64+
65+ [ DefaultValue ( typeof ( Color ) , "210, 210, 215" ) ]
66+ public Color BorderColor
67+ {
68+ get => borderColor ;
69+ set { borderColor = value ; Invalidate ( ) ; }
70+ }
71+
72+ [ DefaultValue ( typeof ( Color ) , "255, 255, 255" ) ]
73+ public Color ButtonColor
74+ {
75+ get => buttonColor ;
76+ set { buttonColor = backgroundColor = value ; Invalidate ( ) ; }
77+ }
78+
79+ [ DefaultValue ( typeof ( Color ) , "100, 100, 100" ) ]
80+ public Color ArrowColor
81+ {
82+ get => arrowColor ;
83+ set { arrowColor = value ; Invalidate ( ) ; }
84+ }
85+
86+ [ DefaultValue ( 8 ) ]
87+ public int BorderRadius
88+ {
89+ get => borderRadius ;
90+ set { borderRadius = value ; Invalidate ( ) ; }
91+ }
92+
93+ private void InitializeAnimation ( )
94+ {
95+ animationTimer = new Timer { Interval = 32 } ;
96+ animationTimer . Tick += ( s , e ) => UpdateAnimation ( ) ;
97+ animationTimer . Start ( ) ;
98+ currentBorderColor = targetBorderColor = borderColor ;
99+ }
100+
101+ private void UpdateAnimation ( )
102+ {
103+ if ( isPainting || ! Visible ) return ;
104+
105+ bool needsUpdate = false ;
106+
107+ if ( Math . Abs ( currentBorderWidth - targetBorderWidth ) > 0.05f )
108+ {
109+ currentBorderWidth += ( targetBorderWidth - currentBorderWidth ) * 0.2f ;
110+ needsUpdate = true ;
111+ }
112+
113+ if ( currentBorderColor != targetBorderColor )
114+ {
115+ currentBorderColor = ColorLerp ( currentBorderColor , targetBorderColor , 0.15f ) ;
116+ needsUpdate = true ;
117+ }
118+
119+ if ( Math . Abs ( arrowScale - targetArrowScale ) > 0.05f )
120+ {
121+ arrowScale += ( targetArrowScale - arrowScale ) * 0.3f ;
122+ needsUpdate = true ;
123+ }
124+
125+ if ( needsUpdate ) Invalidate ( ) ;
126+ }
127+
128+ private Color ColorLerp ( Color c1 , Color c2 , float t ) => Color . FromArgb (
129+ ( int ) ( c1 . R + ( c2 . R - c1 . R ) * t ) ,
130+ ( int ) ( c1 . G + ( c2 . G - c1 . G ) * t ) ,
131+ ( int ) ( c1 . B + ( c2 . B - c1 . B ) * t )
132+ ) ;
133+
134+ private void UpdateAnimationTargets ( )
135+ {
136+ targetBorderWidth = isFocused ? 2.2f : isHovered ? 1.8f : 1.2f ;
137+ targetBorderColor = isFocused ? focusBorderColor : isHovered ? hoverBorderColor : borderColor ;
138+ targetArrowScale = isHovered ? 1.1f : 1.0f ;
139+ }
140+
141+ private void InitializeColors ( )
142+ {
143+ borderColor = buttonColor = backgroundColor = MyMainForm . ButtonMain ;
144+ arrowColor = ForeColor = MyMainForm . FormFore ;
145+
146+ if ( MyMainForm . IsDarkTheme ( ) )
147+ {
148+ hoverBorderColor = Color . FromArgb ( 255 , 145 , 60 ) ;
149+ focusBorderColor = Color . FromArgb ( 255 , 107 , 0 ) ;
150+ hoverArrowColor = Color . FromArgb ( 255 , 107 , 0 ) ;
151+ }
152+ else
153+ {
154+ hoverBorderColor = Color . FromArgb ( 255 , 145 , 60 ) ;
155+ focusBorderColor = Color . FromArgb ( 255 , 107 , 0 ) ;
156+ hoverArrowColor = Color . FromArgb ( 255 , 107 , 0 ) ;
157+ }
158+
159+ currentBorderColor = targetBorderColor = borderColor ;
160+ }
161+
162+ private void UpdateArrowRect ( )
163+ {
164+ int size = 16 , margin = 8 ;
165+ arrowRect = new Rectangle ( Width - size - margin , ( Height - size ) / 2 , size , size ) ;
166+ }
167+
168+ protected override void OnPaint ( PaintEventArgs e )
169+ {
170+ if ( isPainting ) return ;
171+ isPainting = true ;
172+
173+ try
174+ {
175+ var g = e . Graphics ;
176+ g . SmoothingMode = SmoothingMode . AntiAlias ;
177+ g . TextRenderingHint = System . Drawing . Text . TextRenderingHint . ClearTypeGridFit ;
178+
179+ // 清除背景
180+ using ( var brush = new SolidBrush ( Parent ? . BackColor ?? SystemColors . Control ) )
181+ g . FillRectangle ( brush , ClientRectangle ) ;
182+
183+ // 绘制背景和边框
184+ using ( var path = CreateRoundedRectanglePath ( ClientRectangle , borderRadius ) )
185+ using ( var brush = new SolidBrush ( backgroundColor ) )
186+ g . FillPath ( brush , path ) ;
187+
188+ using ( var path = CreateRoundedRectanglePath ( new Rectangle ( 0 , 0 , Width - 1 , Height - 1 ) , borderRadius ) )
189+ using ( var pen = new Pen ( currentBorderColor , currentBorderWidth ) )
190+ g . DrawPath ( pen , path ) ;
191+
192+ // 绘制文本
193+ var text = SelectedItem ? . ToString ( ) ?? ( Items . Count > 0 ? Items [ 0 ] . ToString ( ) : "" ) ;
194+ if ( ! string . IsNullOrEmpty ( text ) )
195+ {
196+ using ( var brush = new SolidBrush ( ForeColor ) )
197+ {
198+ var textRect = new Rectangle ( 12 , 0 , Width - arrowRect . Width - 20 , Height ) ;
199+ var format = new StringFormat { LineAlignment = StringAlignment . Center , Trimming = StringTrimming . EllipsisCharacter } ;
200+ g . DrawString ( text , Font , brush , textRect , format ) ;
201+ }
202+ }
203+
204+ // 绘制箭头
205+ DrawArrowIcon ( g , arrowRect ) ;
206+ }
207+ finally { isPainting = false ; }
208+ }
209+
210+ protected override void OnDrawItem ( DrawItemEventArgs e )
211+ {
212+ if ( e . Index < 0 ) return ;
213+
214+ var isSelected = ( e . State & DrawItemState . Selected ) == DrawItemState . Selected ;
215+ var backColor = isSelected ? Color . FromArgb ( 255 , 235 , 225 ) : BackColor ;
216+ var textColor = isSelected ? Color . FromArgb ( 255 , 107 , 0 ) : ForeColor ;
217+
218+ // 绘制背景
219+ e . Graphics . FillRectangle ( new SolidBrush ( backColor ) , e . Bounds ) ;
220+
221+ // 绘制文本
222+ using ( var brush = new SolidBrush ( textColor ) )
223+ {
224+ var textRect = new Rectangle ( e . Bounds . X + 8 , e . Bounds . Y , e . Bounds . Width - 16 , e . Bounds . Height ) ;
225+ var format = new StringFormat { LineAlignment = StringAlignment . Center , Trimming = StringTrimming . EllipsisCharacter } ;
226+ e . Graphics . DrawString ( GetItemText ( Items [ e . Index ] ) , Font , brush , textRect , format ) ;
227+ }
228+
229+ // 绘制焦点框
230+ if ( ( e . State & DrawItemState . Focus ) == DrawItemState . Focus )
231+ e . DrawFocusRectangle ( ) ;
232+ }
233+
234+ private void DrawArrowIcon ( Graphics g , Rectangle iconRect )
235+ {
236+ if ( iconRect . Width <= 0 ) UpdateArrowRect ( ) ;
237+
238+ var color = isHovered || isFocused ? hoverArrowColor : arrowColor ;
239+
240+ if ( cachedArrowBitmap == null || lastArrowColor != color )
241+ {
242+ cachedArrowBitmap ? . Dispose ( ) ;
243+ cachedArrowBitmap = GenerateArrowIcon ( iconRect . Size , color ) ;
244+ lastArrowColor = color ;
245+ }
246+
247+ if ( cachedArrowBitmap != null )
248+ {
249+ var rect = new Rectangle (
250+ iconRect . X + ( int ) ( iconRect . Width * ( 1 - arrowScale ) / 2 ) ,
251+ iconRect . Y + ( int ) ( iconRect . Height * ( 1 - arrowScale ) / 2 ) ,
252+ ( int ) ( iconRect . Width * arrowScale ) ,
253+ ( int ) ( iconRect . Height * arrowScale )
254+ ) ;
255+ g . DrawImage ( cachedArrowBitmap , rect ) ;
256+ }
257+ }
258+
259+ private Bitmap GenerateArrowIcon ( Size size , Color color )
260+ {
261+ try
262+ {
263+ var svg = new SvgDocument { Width = size . Width , Height = size . Height , ViewBox = new SvgViewBox ( 0 , 0 , 24 , 24 ) } ;
264+ svg . Children . Add ( new SvgPath
265+ {
266+ PathData = SvgPathBuilder . Parse ( "M7 10l5 5 5-5z" ) ,
267+ Fill = new SvgColourServer ( color )
268+ } ) ;
269+ return svg . Draw ( size . Width , size . Height ) ;
270+ }
271+ catch
272+ {
273+ return GenerateFallbackArrowIcon ( size , color ) ;
274+ }
275+ }
276+
277+ private Bitmap GenerateFallbackArrowIcon ( Size size , Color color )
278+ {
279+ var bmp = new Bitmap ( size . Width , size . Height ) ;
280+ using ( var g = Graphics . FromImage ( bmp ) )
281+ {
282+ g . SmoothingMode = SmoothingMode . AntiAlias ;
283+ var points = new [ ] {
284+ new Point ( size . Width / 4 , size . Height / 3 ) ,
285+ new Point ( size . Width * 3 / 4 , size . Height / 3 ) ,
286+ new Point ( size . Width / 2 , size . Height * 2 / 3 )
287+ } ;
288+ g . FillPolygon ( new SolidBrush ( color ) , points ) ;
289+ }
290+ return bmp ;
291+ }
292+
293+ private GraphicsPath CreateRoundedRectanglePath ( Rectangle rect , int radius )
294+ {
295+ var path = new GraphicsPath ( ) ;
296+ float diam = radius * 2f ;
297+ if ( diam > rect . Width ) diam = rect . Width ;
298+ if ( diam > rect . Height ) diam = rect . Height ;
299+
300+ path . AddArc ( rect . X , rect . Y , diam , diam , 180 , 90 ) ;
301+ path . AddArc ( rect . Right - diam , rect . Y , diam , diam , 270 , 90 ) ;
302+ path . AddArc ( rect . Right - diam , rect . Bottom - diam , diam , diam , 0 , 90 ) ;
303+ path . AddArc ( rect . X , rect . Bottom - diam , diam , diam , 90 , 90 ) ;
304+ path . CloseFigure ( ) ;
305+ return path ;
306+ }
307+
308+ // 修复下拉菜单显示问题的关键方法
309+ private void OnDropDown ( object sender , EventArgs e )
310+ {
311+ // 确保下拉列表正常显示
312+ BeginInvoke ( new Action ( ( ) => {
313+ if ( DroppedDown )
314+ {
315+ // 强制重绘以确保下拉列表正确显示
316+ Invalidate ( ) ;
317+ }
318+ } ) ) ;
319+ }
320+
321+ // 重写 WndProc 以确保鼠标事件正确处理
322+ protected override void WndProc ( ref Message m )
323+ {
324+ const int WM_LBUTTONDOWN = 0x201 ;
325+ const int WM_LBUTTONUP = 0x202 ;
326+
327+ if ( m . Msg == WM_LBUTTONDOWN )
328+ {
329+ // 确保点击时下拉列表正常显示
330+ if ( ! DroppedDown )
331+ {
332+ DroppedDown = true ;
333+ return ;
334+ }
335+ }
336+ else if ( m . Msg == WM_LBUTTONUP )
337+ {
338+ // 允许正常的鼠标释放处理
339+ base . WndProc ( ref m ) ;
340+ return ;
341+ }
342+
343+ base . WndProc ( ref m ) ;
344+ }
345+
346+ protected override void OnResize ( EventArgs e )
347+ {
348+ base . OnResize ( e ) ;
349+ UpdateArrowRect ( ) ;
350+ cachedArrowBitmap ? . Dispose ( ) ;
351+ cachedArrowBitmap = null ;
352+ Invalidate ( ) ;
353+ }
354+
355+ protected override void Dispose ( bool disposing )
356+ {
357+ if ( disposing )
358+ {
359+ animationTimer ? . Stop ( ) ;
360+ animationTimer ? . Dispose ( ) ;
361+ cachedArrowBitmap ? . Dispose ( ) ;
362+ }
363+ base . Dispose ( disposing ) ;
364+ }
365+ }
366+ }
0 commit comments