Winform控件優化之圓角按鈕2
前言
接上一篇Winform控件優化之圓角按鈕1繼續介紹圓角按鈕的實現和優化,以及這個過程中遇到的問題…
圓角按鈕實現的進一步優化
【最終實現,兼容默認按鈕】
主要功能【圓角方面】
結合前面兩部分介紹和代碼,最終優化實現ButtonPro按鈕(繼承自Button),既提供Button原生功能,又提供擴展功能,除瞭圓角以外,還實現瞭圓形、圓角矩形的腳尖效果、邊框大小和顏色、背景漸變顏色、圓角的繪制模式和創建Region模式、常用按鈕圖標快速使用(此項暫未實現)等其他功能。
圓角按鈕控件相關屬性和實現:
RoundRadius
:圓角半徑,>=0時啟用圓角按鈕,等於0為直角(但可使用背景色等所有Round圓角相關屬性),<0時使用默認Button樣式。RegionNewModel
:創建新Region的模式,使用'繪制范圍'創建新的Region,實現控件區域貼合繪制范圍,實現圖形外的部分"正確的透明",但相對會有些鋸齒。ShowCusp
:是否顯示尖角,默認不顯示,當啟用Radius圓角(RoundRadius>=0)時才有效。CuspAlign
:(三角)尖角的顯示位置,當啟用圓角按鈕(RoundRadius>=0),且顯示尖角時有效。RoundBorderWidth
:啟用Radius圓角(RoundRadius>=0)時邊框寬度,默認0。RoundBorderColor
:啟用Radius圓角(RoundRadius>=0)時邊框顏色,默認黑色。EnableBGGradient
:啟用漸變背景色(需要RoundRadius>=0),啟用後RoundNormalColor、RoundHoverColor、RoundPressedColor顏色無效。GradientModel
:線性漸變的模式,默認垂直漸變。BGColorBegin
:漸變開始色。BGColorEnd
:漸變結束色。RoundNormalColor
:啟用Radius圓角(RoundRadius>=0)時按鈕標準顏色。RoundHoverColor
:啟用Radius圓角(RoundRadius>=0)鼠標位於按鈕上時的按鈕顏色。RoundPressedColor
:啟用Radius圓角(RoundRadius>=0)鼠標按下時的按鈕顏色。- 圓形按鈕,長寬一樣,圓角半徑為長寬的一半,可實現圓形按鈕。
- 擴展控件屬性分類修改為“高級”,使用
CategoryAttribute
特性。
註意:
borderPen繪制線條的對齊方式:
borderPen.Alignment = PenAlignment.Inset;
。但是指定Inset繪制邊框也有小問題,如果是重新創建Region,則繪制邊框後內部變為直角矩形(先填充再繪制邊框線條);如果使用就有Region(不新建),也會變成內部直角,並且如果不指定Inset則會外部繪制的Border線條變成直接(由原本直角的Region承載圓角之外的部分)。總之都有些問題,可自行測試。
在繪制邊框時,不推薦使用
PenAlignment.Inset
,通過計算減少Rectangle的范圍為半個Border的路徑范圍(寬高-RoundBorderWidth),繪制時在路徑內外正好有一個Border的大小來實現。而且這樣不會發生上面介紹的內或外直角而非圓角的情況
OnPaint方法中不要使用e.ClipRectangle
應該使用控件的寬高(Width、Height)計算Rectangle矩形,或者使用
ClientRectangle
屬性。
相互影響的問題,復制的或拖拽的ButtonPro控件,會被其他控件影響,即調整某個控件,會導致ButtonPro的一個或多個也會牽連變化(很雜亂的關聯變化),如何解決?應該是獨立的才對!且在設計器中沒法通過撤銷操作還原效果
與SetStyle設置無關,後面測試和重新指定Region有關,在後續測試發現最終導致此類問題的原因,在於使用瞭OnPaint參數e.ClipRectangle
作為控件繪制范圍繪制產生的。【比如rect = e.Graphics.DrawRoundRectAndCusp(e.ClipRectangle, roundRadius, baseColor, showCusp, cuspAlign)
】
直接使用控件的Width
和Height
定義繪制繪制范圍,替換到e.ClipRectangle
。
OnPaint方法中不要使用
e.ClipRectangle
作為控件繪制范圍以下問題均是由於OnPaint方法中使用
e.ClipRectangle
來繪制繪制范圍導致的(它是個自動被影響的值,應該使用Width、Height創建)不重新賦值Region,拖動或調整按鈕,會重寫顯示相互影響:
如果賦值新的Region,當調整或移動控件時,就有可能影響顯示的佈局或大小,且無法通過撤銷還原
重新創建Region的鋸齒問題和優勢
【寫錯代碼實現圓形導致的錯誤探索,但也很有意義】
重新賦值Region
一個最大的確實是會產生一些鋸齒,即使使用抗鋸齒和最好質量繪制,要想繪制圓形效果,必須重新賦值Region
,否則控件隻會是圓角。因此,提供瞭RoundCircleModel
屬性,用於是否啟用可繪制圓形按鈕的模式,會有些鋸齒,默認不啟用,如果需要時啟用即可。
需要記住的幾點:
Region
定義的是控件的區域,通過GDI+繪制可以實現一個自定義的區域,從而解除默認的寬高矩形區域控件的限制。- 重新定義和賦值
Region
的缺點是會產生一定鋸齒,這個鋸齒是Region
產生的,而不是GDI+繪制填充產生的。 - 無法消除創建的
Region
鋸齒,至少沒提供相關API,因此實際重繪控件時,通常不要創建新的Region
- 由於設置中使用瞭Winform透明父控件的樣式,因此要註意其正確的父控件設置。
直接使用繪制後的正確的繪制范圍創建新的Region
區域,則沒有透明父控件的問題,可以實現“正確的透明”,具體可參加下圖所示【新建Region
繪制圖形後圓角邊緣部分出現的1像素的控件顏色可以通過調整創建Region
時和繪制時的范圍消除(多餘的1像素白邊問題無法簡單的通過調整Region
和繪制范圍解決,具體可自行測試)】。
【若是消除新建Region的鋸齒問題,將會非常完美】
若是能實現直接繪制無鋸齒的圓角Region區域,則,直接在Paint事件中實現控件的圓角Region即可,無需在額外重新繪制背景和文字。【更簡單、更完美的方案】
目前所知,無法對Region進行抗鋸齒,即使使用使用GDI+的API
CreateRoundRectRgn
方法。相關介紹參見 c# How to make smooth arc region using graphics path、Winforms: Smooth the rounded edges for panel控件上重繪可以使用
Graphics
對象在背景透明的Region區域控件上實現,其背後至少一個父控件(或頂層的Form窗體)。但是,對於一個Form,要想實現圓角或多邊形窗體,則必須重新生成Region,但創建Region在不規則形狀時邊緣鋸齒無法解決(如果有大牛,應該可以應用消除鋸齒的算法),後面會介紹一種取巧或者Win32窗體推薦的一種方式,即,使用Layered Windows。
代碼具體實現
將繪制方法精簡為擴展方法後,擴展控件的全部源代碼如下:
擴展方法參見Winform控件優化Paint事件實現圓角組件及提取繪制圓角的方法
using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; namespace CMControls { public class ButtonPro : Button { private int roundRadius;//半徑 private bool showCusp = false;//顯示尖角 private RectangleAlign cuspAlign = RectangleAlign.RightTop;//三角尖角位置 private Color roundBorderColor = Color.Black;//邊框顏色 private int roundBorderWidth = 0;//邊框寬度 private Color roundHoverColor = Color.FromArgb(220, 80, 80);//鼠標位於控件時顏色 private Color roundNormalColor = Color.FromArgb(51, 161, 224);//基顏色 private Color roundPressedColor = Color.FromArgb(251, 161, 0);//鼠標按下控件時基顏色 // 鼠標相對控件的狀態位置,對應上面不同顏色 private MouseControlState mouseControlState = MouseControlState.Normal; private bool regionNewModel = false; // 創建新Region的模式,使用"繪制范圍"創建新的Region,實現控件區域貼合繪制范圍,實現圖形外的部分"正確的透明",但相對會有些鋸齒 private Color beginBGColor; //= Color.FromArgb(251, 161, 0);//漸變開始色 private Color endBGColor; //= Color.FromArgb(251, 161, 0);//漸變結束色 private bool enableBGGradient = false; //使用漸變色 private LinearGradientMode gradientModel = LinearGradientMode.Vertical; //線性漸變的模式 private Region originRegion; /// <summary> /// 圓形按鈕的半徑屬性 /// </summary> [CategoryAttribute("高級"), DefaultValue(20), Description("圓角半徑,>=0時啟用圓角按鈕,等於0為直角(但可使用背景色等所有Round圓角相關屬性),<0時使用默認Button樣式")] public int RoundRadius { set { roundRadius = value; // 使控件的整個畫面無效並重繪控件 this.Invalidate(); } get { return roundRadius; } } /// <summary> /// 圓角下創建新Region模式 /// </summary> [CategoryAttribute("高級"), DefaultValue(false), Description("創建新Region的模式,使用'繪制范圍'創建新的Region,實現控件區域貼合繪制范圍,實現'正確的透明',但相對會有些的鋸齒")] public bool RegionNewModel { set { regionNewModel = value; this.Invalidate(); } get { return regionNewModel; } } /// <summary> /// 三角尖角位置,當啟用圓角 /// </summary> [CategoryAttribute("高級"), Description("(三角)尖角的顯示位置,當啟用圓角按鈕(RoundRadius>=0),且顯示尖角時有效"), DefaultValue(RectangleAlign.RightTop)] public RectangleAlign CuspAlign { set { cuspAlign = value; this.Invalidate(); } get { return cuspAlign; } } [CategoryAttribute("高級"), Description("是否顯示尖角,默認不顯示,當啟用Radius圓角(RoundRadius>=0)時才有效"), DefaultValue(false)] public bool ShowCusp { set { showCusp = value; this.Invalidate(); } get { return showCusp; } } [CategoryAttribute("高級"), DefaultValue(0), Description("啟用Radius圓角(RoundRadius>=0)時邊框寬度,默認0")] public int RoundBorderWidth { set { roundBorderWidth = value; this.Invalidate(); } get { return roundBorderWidth; } } [CategoryAttribute("高級"), DefaultValue(typeof(Color), "0, 0, 0"), Description("啟用Radius圓角(RoundRadius>=0)時邊框顏色,默認黑色")] public Color RoundBorderColor { get { return this.roundBorderColor; } set { this.roundBorderColor = value; this.Invalidate(); } } /// <summary> /// 是否啟用背景漸變色,啟用後RoundNormalColor、RoundHoverColor、RoundPressedColor顏色無效 /// </summary> [CategoryAttribute("高級"), DefaultValue(false), Description("啟用漸變背景色(需要RoundRadius>=0),啟用後RoundNormalColor、RoundHoverColor、RoundPressedColor顏色無效")] public bool EnableBGGradient { get { return this.enableBGGradient; } set { this.enableBGGradient = value; this.Invalidate(); } } /// <summary> /// 線性漸變的模式,默認垂直漸變 /// </summary> [CategoryAttribute("高級"), DefaultValue(LinearGradientMode.Vertical), Description("線性漸變的模式,默認垂直漸變")] public LinearGradientMode GradientModel { get { return this.gradientModel; } set { this.gradientModel = value; this.Invalidate(); } } /// <summary> /// 背景漸變色 /// </summary> [CategoryAttribute("高級"), DefaultValue(typeof(Color), "0, 122, 204"), Description("漸變開始色")] public Color BGColorBegin { get { return this.beginBGColor; } set { this.beginBGColor = value; this.Invalidate(); } } /// <summary> /// 背景漸變色 /// </summary> [CategoryAttribute("高級"), DefaultValue(typeof(Color), "8, 39, 57"), Description("漸變結束色")] public Color BGColorEnd { get { return this.endBGColor; } set { this.endBGColor = value; this.Invalidate(); } } [CategoryAttribute("高級"), DefaultValue(typeof(Color), "51, 161, 224"), Description("啟用Radius圓角(RoundRadius>=0)時按鈕標準顏色")] public Color RoundNormalColor { get { return this.roundNormalColor; } set { this.roundNormalColor = value; this.Invalidate(); } } [CategoryAttribute("高級"), DefaultValue(typeof(Color), "220, 80, 80"), Description("啟用Radius圓角(RoundRadius>=0)鼠標位於按鈕上時的按鈕顏色")] public Color RoundHoverColor { get { return this.roundHoverColor; } set { this.roundHoverColor = value; this.Invalidate(); } } [CategoryAttribute("高級"), DefaultValue(typeof(Color), "251, 161, 0"), Description("啟用Radius圓角(RoundRadius>=0)鼠標按下時的按鈕顏色")] public Color RoundPressedColor { get { return this.roundPressedColor; } set { this.roundPressedColor = value; this.Invalidate(); } } protected override void OnMouseEnter(EventArgs e)//鼠標進入時 { mouseControlState = MouseControlState.Hover;//Hover base.OnMouseEnter(e); } protected override void OnMouseLeave(EventArgs e)//鼠標離開 { mouseControlState = MouseControlState.Normal;//正常 base.OnMouseLeave(e); } protected override void OnMouseDown(MouseEventArgs e)//鼠標按下 { if (e.Button == MouseButtons.Left && e.Clicks == 1)//鼠標左鍵且點擊次數為1 { mouseControlState = MouseControlState.Pressed;//按下的狀態 } base.OnMouseDown(e); } protected override void OnMouseUp(MouseEventArgs e)//鼠標彈起 { if (e.Button == MouseButtons.Left && e.Clicks == 1) { if (ClientRectangle.Contains(e.Location))//控件區域包含鼠標的位置 { mouseControlState = MouseControlState.Hover; } else { mouseControlState = MouseControlState.Normal; } } base.OnMouseUp(e); } public ButtonPro() { ForeColor = Color.White; this.FlatStyle = FlatStyle.Flat; this.FlatAppearance.BorderSize = 0; FlatAppearance.MouseDownBackColor = Color.Transparent; FlatAppearance.MouseOverBackColor = Color.Transparent; FlatAppearance.CheckedBackColor = Color.Transparent; RoundRadius = 20; // 似乎當值為默認20時重新生成設計器或者重新打開項目後,此屬性就會變為0,必須在構造函數中指定20來解決 this.mouseControlState = MouseControlState.Normal; // 原始Region originRegion = Region; } public override void NotifyDefault(bool value) { base.NotifyDefault(false); // 去除窗體失去焦點時最新激活的按鈕邊框外觀樣式 } //重寫OnPaint protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); //base.OnPaintBackground(e); // 不能使用 e.ClipRectangle.GetRoundedRectPath(_radius) 計算控件全部的Region區域,e.ClipRectangle 似乎是變化的,必須使用固定的Width和Height,包括下面的繪制也不能使用e.ClipRectangle // 在Paint事件中也不推薦使用e.ClipRectangle時沒問題的 Rectangle controlRect = new Rectangle(0, 0, this.Width, this.Height); // roundRadius 修改回來是要還原 if (roundRadius >= 0 && regionNewModel) // 圓角下創建新Region模式,使用自定義Region { var controlPath = controlRect.GetRoundedRectPath(roundRadius); // 要在繪制之前指定Region,否則無效 this.Region = new Region(controlPath); } else // 修改對應調整 { //this.Region = new Region(controlRect);//也屬於重新修改 this.Region = originRegion; } if (roundRadius >= 0) { Rectangle rect; if (enableBGGradient) { rect = e.Graphics.DrawRoundRectAndCusp(controlRect, roundRadius, beginBGColor, endBGColor, true, CuspAlign, gradientModel, roundBorderWidth > 0 ? new Pen(roundBorderColor, roundBorderWidth) : null); } else { Color baseColor; switch (mouseControlState) { case MouseControlState.Hover: baseColor = this.roundHoverColor; break; case MouseControlState.Pressed: baseColor = this.roundPressedColor; break; case MouseControlState.Normal: baseColor = this.roundNormalColor; break; default: baseColor = this.roundNormalColor; break; } rect = e.Graphics.DrawRoundRectAndCusp(controlRect, roundRadius, baseColor, showCusp, cuspAlign, roundBorderWidth > 0 ? new Pen(roundBorderColor, roundBorderWidth) : null); } // 使用合適的區域 e.Graphics.DrawText(rect, Text, ForeColor, Font, TextAlign); } } } }
測試擴展按鈕控件ButtonPro
通過拖拽ButtonPro按鈕控件,調整各個參數,查看不同樣式的按鈕效果。
TextRenderer.DrawText繪制文本
文本垂直居中偏上的問題及文字大小不正確【推薦使用TextRenderer.DrawText繪制文本】
所有的一切都非常好,但是,目前還有一個小問題,就是繪制垂直居中的文本時,可以明顯看到偏上方。是的由此產生“瑕疵”。
目前沒有找到很好的解決辦法,更換字體、字體大小為偶數會有一定效果,但並不能完全解決。
使用StringFormat.GenericTypographic
後面經過花木蘭控件庫的大佬提醒,使用StringFormat.GenericTypographic
作為文本繪制的格式對象,可以看到偏上的問題有瞭明顯改善。
using (StringFormat strF = StringFormat.GenericTypographic) { // 文字佈局 switch (_textAlign) { //... } g.DrawString(text, font, brush, rect, strF); }
雖然如此,但是還是有一點點不完全垂直。而且對比同樣字體情況下,DrawString繪制出來的文本明顯和原生Button時顯示的文字有很大差別(大小、清晰度)
僅僅重寫OnPaintBackground
【無效果】
後面由於文字繪制的問題,想著直接重寫OnPaintBackground
,文字交由Winform自己繪制,應該可以達到很好的效果。
但是,但是重寫OnPaintBackground
後背景沒有任何效果,僅僅是設置的透明背景,無法實現圓角等各種繪制。
目前暫時不知道該如何正確的處理OnPaintBackground方法。
使用TextRenderer.DrawText繪制文本
【不推薦Graphics.DrawString】
後來幾乎要放棄瞭,因為最終繪制的文字確實很不理想,和原生Button對比起來差好多。。。
然後想著測試下TextRenderer.DrawText()
繪制文本的效果如何,最終發現文字繪制效果非常好(大小正確、清晰),重點是文字位置的水平和垂直居中沒有任何問題,基本和原生Button的文字效果一致。
// ... TextRenderer.DrawText(g, text, font, rect, color, formatFlags);
擴展方法的源碼參見Winform控件優化Paint事件實現圓角組件及提取繪制圓角的方法
到此這篇關於Winform控件優化之圓角按鈕2的文章就介紹到這瞭,更多相關Winform 圓角按鈕內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!