引言
我們有時候會在程序的文件夾里看見一些圖標,而這些圖標恰好是作為按鈕的背景圖片來使用的。鼠標指針在處於不同狀態時,有“進入按鈕”、“按下左鍵”,“松開”,“離開按鈕”,則按鈕的背景圖片也在發生改變。這些圖片大致如下(來自愛奇藝萬能播放器PC端):
全文僅以第一張圖片素材為例,這張圖片可以分為4段(下圖所示),恰好表示鼠標指針在操作控件時各個不同的狀態,從左到右依次表示為“初始狀態”(默認顯示的背景)、“指針進入按鈕區域或鼠標左鍵松開”,“鼠標左鍵按下不動”,“鼠標指針離開按鈕區域”
本身這個圖片素材設計的就很巧妙,它的尺寸是164 * 41,因此每一段的尺寸剛好是41 * 41
在貼代碼之前請先看看效果:
編譯環境及說明
- Microsoft Visual Studio 2010
- C# .Net Framework 4.0
- 除了實現這個圖片按鈕的功能,還添加了一些代碼來減少甚至防止圖片按鈕在與鼠標指針交互時的閃爍
圖片素材分割
顯然上述的圖片素材需要分割為4段作為鼠標指針的不同狀態,實現分割的思路為
-
- 把圖片轉換為Image對象
- 克隆該Image對象(防止直接操作控件背景導致問題)
- 創建元素類型為Bitmap的容器List,用於存放分割后的4個圖片對象
- 定義矩形區域Rectangle結構體,它用來表明應該取整個圖片素材中的哪個部分,用for循環逐個計算出這4段圖片的左上角坐標(即起始坐標)、寬度、高度,再將值對應的賦予Rectangle結構體中的屬性
- 克隆上一步Rectangle結構體所對應區域下的圖片塊,並添加到第3步中提到的List容器中並返回該容器
由此可以定義一個函數 ImageSplit ,代碼如下
1 /// <summary> 2 /// 圖片分割函數,此處僅僅按圖片寬度來分割 3 /// </summary> 4 /// <param name="ImageWidth">圖片素材寬度</param> 5 /// <param name="SegmentsNum">要分割為幾段,默認是1段</param> 6 /// <returns>分割后的圖片集合</returns> 7 private List<Bitmap> ImageSplit(int ImageWidth, int SegmentsNum = 1) 8 { 9 // 定義分割后的圖片存放容器 10 List<Bitmap> SplitedImage = new List<Bitmap>(); 11 // 克隆按鈕背景圖片 12 Bitmap SrcBmp = new Bitmap(this.Image); 13 // 指定圖片像素格式為ARGB型 14 PixelFormat ReslouteFormat = PixelFormat.Format32bppArgb; 15 // 指定分割區域 16 Rectangle SplitAreaRec = new Rectangle(); 17 // 如果圖片尺寸為負值 18 if (ImageWidth <= 0 || SegmentsNum <= 0) 19 return SplitedImage; 20 else 21 { 22 // 依據要分割的段數來做循環 23 // 從 0(含) 到 SegmentsNum - 1(含) 24 for (int i = 0; i < SegmentsNum; i++) 25 { 26 /* 27 * 在這里要把圖片分割為4段小圖片,每一段圖片大小均為41 * 41 28 * 以下列舉出每個小圖片的左上角坐標(即起始坐標) 29 * (0, 0) 30 * (41, 0) 31 * (82, 0) 32 * (123, 0) 33 * Y 坐標均為 0 34 * 35 * 計算每個小圖片的寬度:ImageWidth / SegmentsNum (總寬度/要分割的段數) 36 * 因此 X = i * (ImageWidth / SegmentsNum) 37 */ 38 SplitAreaRec.X = 0 + i * (ImageWidth / SegmentsNum); 39 SplitAreaRec.Y = 0; 40 // 小圖片為正方形,所以以下這兩個值一樣 41 SplitAreaRec.Width = ImageWidth / SegmentsNum; 42 SplitAreaRec.Height = ImageWidth / SegmentsNum; 43 // 以指定的像素格式,克隆分割的圖像 44 Bitmap SplitedBmp = SrcBmp.Clone(SplitAreaRec, ReslouteFormat); 45 // 添加進集合 46 SplitedImage.Add(SplitedBmp); 47 } 48 GC.Collect(); 49 return SplitedImage; 50 } 51 }
事件處理
該圖片按鈕控件有幾個事件需要處理,包括:
-
- OnPaint(控件繪制事件)
- OnMouseEnter(鼠標指針進入控件區域觸發事件)
- OnMouseDown (鼠標左鍵按下)
- OnMouseUp (鼠標左鍵松開)
- OnMouseLeave(鼠標指針離開控件區域)
OnPaint事件
首先在自定義控件類中定義私有對象,緩沖 Image 對象(最開始為空白圖形)和對應的緩沖 Graphics 對象(在空白圖形上繪制圖案),這是為了減少閃爍
Image buffImg;
Graphics buffImgG;
具體代碼如下:

1 protected override void OnPaint(PaintEventArgs pe) 2 { 3 base.OnPaint(pe); 4 // 創建空圖形 5 buffImg = new Bitmap(Width,Height); 6 // 根據空圖形創建畫布Graphics對象 7 buffImgG = Graphics.FromImage(buffImg); 8 // 用畫布對象,以背景色刷新空圖形 9 buffImgG.Clear(this.BackColor); 10 11 //雙三次插值 12 pe.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; 13 //抗鋸齒 14 pe.Graphics.SmoothingMode = SmoothingMode.AntiAlias; 15 16 //圖形軌跡 17 GraphicsPath gp = new GraphicsPath(); 18 //限定圓形繪制長方形區域 19 //限定為正方形 20 Rectangle limitedRec = new Rectangle(); 21 Point startDrawingPoint = new Point(0, 0); 22 limitedRec.Location = startDrawingPoint; 23 limitedRec.Size = new Size(Width - 1, Height - 1); 24 25 if (IsWeightWidthEqual) 26 { 27 int fixedWidth = Width - 1; 28 Height = Width; 29 Width = Height; 30 limitedRec.Size = new Size(fixedWidth, fixedWidth); 31 } 32 //以下代碼視為了把圖片框的顯示邊界改成圓形 33 //添加軌跡為橢圓 34 gp.AddEllipse(limitedRec); 35 //重新設置邊界 36 Region rg = new Region(gp); 37 this.Region = rg; 38 //銷毀資源 39 rg.Dispose(); 40 gp.Dispose(); 41 }
鼠標交互事件
上述5個事件除 OnPaint 之外,其余均為鼠標交互事件
因為本文對控件閃爍的問題做了處理,所以在重寫(Override)此類事件函數時需要添加一個 BufferedGraphics 對象並為之分配空間,最后再使用它來渲染(Render)繪制好的圖形至當前控件的 Graphics 畫布(/設備)對象(相當於添加一個中間緩沖層將圖形繪制完成后再直接覆蓋到控件背景上以避免閃爍)
以下是OnMouseEnter事件的代碼:

1 //1.鼠標進入 2 protected override void OnMouseEnter(EventArgs e) 3 { 4 base.OnMouseEnter(e); 5 using (Graphics g = Graphics.FromHwnd(this.Handle)) 6 { 7 // 雙三次插值 8 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 9 // 抗鋸齒 10 g.SmoothingMode = SmoothingMode.AntiAlias; 11 // 再次以背景色刷新空白圖形 12 buffImgG.Clear(this.BackColor); 13 // 在空白圖形上繪制分割后的第2個小圖片 14 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[1], ClientRectangle); 15 // 依據上述空白圖形buffImgG創建緩沖Graphics,指定區域為該控件工作區 16 BufferedGraphics buff = BufferedGraphicsManager.Current.Allocate(buffImgG, ClientRectangle); 17 // BufferedGraphics繪制整個圖形,指定繪制區域為該控件工作區 18 // 此處推薦使用DrawImageUnscaledAndClipped 19 buff.Graphics.DrawImageUnscaledAndClipped(buffImg, ClientRectangle); 20 // 圖形緩沖區寫入到當前控件Graphics對象 21 buff.Render(g); 22 } 23 }
其它的鼠標交互事件類似,只是繪制的背景圖片不一樣而已,即這句代碼 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[1], ClientRectangle); 中的 SplitedImage 索引各有不同,就不一一重復了。
代碼匯總
那么完整的程序應該如何運行呢?
在VS2010中新建一個解決方案,其中添加2個項目,一個是WinForm窗體應用程序,這個是用來測試控件的;另一個是Windows窗體控件庫。窗體控件庫默認繼承的是 UserControl 這個類,但是在本文筆者將其改為繼承 PictureBox 類,即自己做的這個控件還是屬於PictureBox這個類型而不是 UserControl
所以完整的代碼如下:

1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Drawing; 5 using System.Drawing.Drawing2D; 6 7 using System.Data; 8 using System.Linq; 9 using System.Text; 10 using System.Windows.Forms; 11 using System.Diagnostics; 12 using System.IO; 13 using System.Security.Cryptography; 14 using System.Drawing.Imaging; 15 16 17 namespace PicBtn 18 { 19 public partial class RoundPictureBox : PictureBox 20 { 21 [Category("派生屬性"), Description("有的圖標是正圓形,因此此處設置控件的長寬是否相等")] 22 public bool IsWeightWidthEqual { get; set; } 23 // 該屬性尚未使用 24 [Category("派生屬性"), Description("表明是否由多個圖片來表示圖片框的按鈕特效")] 25 public bool IsMultiImage { get; set; } 26 // 分割后圖片容器 27 List<Bitmap> SplitedImage = null; 28 29 Image buffImg; 30 Graphics buffImgG; 31 public RoundPictureBox() 32 { 33 InitializeComponent(); 34 //雙緩沖區繪制 35 DoubleBuffered = true; 36 SizeMode = PictureBoxSizeMode.Normal; 37 // 圖片素材路徑,視具體情況而定(可以更改) 38 this.ImageLocation = @"D:\文檔\VS項目\PicButton\view_next.png"; 39 //按鈕圖片分割 40 this.Image = Image.FromFile(ImageLocation); 41 // 圖片寬度(Width)164,將其分為4段並放到容器中 42 SplitedImage = ImageSplit(164, 4); 43 } 44 45 46 protected override void OnPaint(PaintEventArgs pe) 47 { 48 base.OnPaint(pe); 49 // 創建空圖形 50 buffImg = new Bitmap(Width,Height); 51 // 根據空圖形創建畫布Graphics對象 52 buffImgG = Graphics.FromImage(buffImg); 53 // 用畫布對象,以背景色刷新空圖形 54 buffImgG.Clear(this.BackColor); 55 56 //雙三次插值 57 pe.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; 58 //抗鋸齒 59 pe.Graphics.SmoothingMode = SmoothingMode.AntiAlias; 60 61 //圖形軌跡 62 GraphicsPath gp = new GraphicsPath(); 63 //限定圓形繪制長方形區域 64 //限定為正方形 65 Rectangle limitedRec = new Rectangle(); 66 Point startDrawingPoint = new Point(0, 0); 67 limitedRec.Location = startDrawingPoint; 68 limitedRec.Size = new Size(Width - 1, Height - 1); 69 70 if (IsWeightWidthEqual) 71 { 72 int fixedWidth = Width - 1; 73 Height = Width; 74 Width = Height; 75 limitedRec.Size = new Size(fixedWidth, fixedWidth); 76 } 77 //以下代碼是為了把圖片框的顯示邊界改成圓形 78 //添加軌跡為橢圓 79 gp.AddEllipse(limitedRec); 80 //重新設置邊界 81 Region rg = new Region(gp); 82 this.Region = rg; 83 //銷毀資源 84 rg.Dispose(); 85 gp.Dispose(); 86 } 87 88 //繪制鼠標進入點擊並離開的圖像 89 /* 90 * 完整的點擊過程如下(只考慮鼠標左鍵的情況) 91 * 1. 鼠標指針進入PictureBox(以下簡稱“該控件”),觸發事件 MouseEnter 92 * 2. 鼠標按下不動的一瞬間,觸發事件 MouseDown 93 * 3. 鼠標松開一瞬間,觸發事件 MouseUp 94 * 4. 鼠標指針離開該控件,觸發事件 MouseLeave 95 */ 96 //1.鼠標進入 97 protected override void OnMouseEnter(EventArgs e) 98 { 99 base.OnMouseEnter(e); 100 using (Graphics g = Graphics.FromHwnd(this.Handle)) 101 { 102 // 雙三次插值 103 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 104 // 抗鋸齒 105 g.SmoothingMode = SmoothingMode.AntiAlias; 106 // 再次以背景色刷新空白圖形 107 buffImgG.Clear(this.BackColor); 108 // 在空白圖形上繪制分割后的第2個小圖片 109 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[1], ClientRectangle); 110 // 依據上述空白圖形buffImgG創建緩沖Graphics,指定區域為該控件工作區 111 BufferedGraphics buff = BufferedGraphicsManager.Current.Allocate(buffImgG, ClientRectangle); 112 // BufferedGraphics繪制整個圖形,指定繪制區域為該控件工作區 113 // 此處推薦使用DrawImageUnscaledAndClipped 114 buff.Graphics.DrawImageUnscaledAndClipped(buffImg, ClientRectangle); 115 // 圖形緩沖區寫入到當前控件Graphics對象 116 buff.Render(g); 117 } 118 } 119 //2.鼠標按下 120 protected override void OnMouseDown(MouseEventArgs e) 121 { 122 base.OnMouseDown(e); 123 using (Graphics g = Graphics.FromHwnd(this.Handle)) 124 { 125 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 126 g.SmoothingMode = SmoothingMode.HighQuality; 127 128 buffImgG.InterpolationMode = InterpolationMode.HighQualityBicubic; 129 buffImgG.SmoothingMode = SmoothingMode.HighQuality; 130 buffImgG.Clear(BackColor); 131 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[2],ClientRectangle); 132 BufferedGraphics buff = BufferedGraphicsManager.Current.Allocate(buffImgG, ClientRectangle); 133 buff.Graphics.DrawImageUnscaledAndClipped(buffImg, ClientRectangle); 134 buff.Render(g); 135 } 136 } 137 138 //3. 鼠標按鍵松開 139 protected override void OnMouseUp(MouseEventArgs e) 140 { 141 base.OnMouseUp(e); 142 using (Graphics g = Graphics.FromHwnd(this.Handle)) 143 { 144 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 145 g.SmoothingMode = SmoothingMode.HighQuality; 146 buffImgG.Clear(BackColor); 147 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[1], ClientRectangle); 148 BufferedGraphics buff = BufferedGraphicsManager.Current.Allocate(buffImgG, ClientRectangle); 149 buff.Graphics.DrawImageUnscaledAndClipped(buffImg, ClientRectangle); 150 buff.Render(g); 151 } 152 } 153 154 //4.鼠標離開 155 protected override void OnMouseLeave(EventArgs e) 156 { 157 base.OnMouseLeave(e); 158 using (Graphics g = Graphics.FromHwnd(this.Handle)) 159 { 160 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 161 g.SmoothingMode = SmoothingMode.HighQuality; 162 163 buffImgG.Clear(BackColor); 164 buffImgG.DrawImageUnscaledAndClipped(SplitedImage[3], ClientRectangle); 165 BufferedGraphics buff = BufferedGraphicsManager.Current.Allocate(buffImgG, ClientRectangle); 166 buff.Graphics.DrawImageUnscaledAndClipped(buffImg, ClientRectangle); 167 buff.Render(g); 168 } 169 } 170 171 /// <summary> 172 /// 圖片分割函數,此處僅僅按圖片寬度來分割 173 /// </summary> 174 /// <param name="ImageWidth">圖片素材寬度</param> 175 /// <param name="SegmentsNum">要分割為幾段,默認是1段</param> 176 /// <returns>分割后的圖片集合</returns> 177 private List<Bitmap> ImageSplit(int ImageWidth, int SegmentsNum = 1) 178 { 179 // 定義分割后的圖片存放容器 180 List<Bitmap> SplitedImage = new List<Bitmap>(); 181 // 克隆按鈕背景圖片 182 Bitmap SrcBmp = new Bitmap(this.Image); 183 // 指定圖片像素格式為ARGB型 184 PixelFormat ReslouteFormat = PixelFormat.Format32bppArgb; 185 // 指定分割區域 186 Rectangle SplitAreaRec = new Rectangle(); 187 // 如果圖片尺寸為負值 188 if (ImageWidth <= 0 || SegmentsNum <= 0) 189 return SplitedImage; 190 else 191 { 192 // 依據要分割的段數來做循環 193 // 從 0(含) 到 SegmentsNum - 1(含) 194 for (int i = 0; i < SegmentsNum; i++) 195 { 196 /* 197 * 在這里要把圖片分割為4段小圖片,每一段圖片大小均為41 * 41 198 * 以下列舉出每個小圖片的左上角坐標(即起始坐標) 199 * (0, 0) 200 * (41, 0) 201 * (82, 0) 202 * (123, 0) 203 * Y 坐標均為 0 204 * 205 * 計算每個小圖片的寬度:ImageWidth / SegmentsNum (總寬度/要分割的段數) 206 * 因此 X = i * (ImageWidth / SegmentsNum) 207 */ 208 SplitAreaRec.X = 0 + i * (ImageWidth / SegmentsNum); 209 SplitAreaRec.Y = 0; 210 // 小圖片為正方形,所以以下這兩個值一樣 211 SplitAreaRec.Width = ImageWidth / SegmentsNum; 212 SplitAreaRec.Height = ImageWidth / SegmentsNum; 213 // 以指定的像素格式,克隆分割的圖像 214 Bitmap SplitedBmp = SrcBmp.Clone(SplitAreaRec, ReslouteFormat); 215 // 添加進集合 216 SplitedImage.Add(SplitedBmp); 217 } 218 GC.Collect(); 219 return SplitedImage; 220 } 221 } 222 } 223 }
還有設計器的代碼:

1 using System.Windows.Forms; 2 namespace PicBtn 3 { 4 partial class RoundPictureBox 5 { 6 /// <summary> 7 /// 必需的設計器變量。 8 /// </summary> 9 private System.ComponentModel.IContainer components = null; 10 11 /// <summary> 12 /// 清理所有正在使用的資源。 13 /// </summary> 14 /// <param name="disposing">如果應釋放托管資源,為 true;否則為 false。</param> 15 protected override void Dispose(bool disposing) 16 { 17 if (disposing && (components != null)) 18 { 19 components.Dispose(); 20 } 21 base.Dispose(disposing); 22 } 23 24 #region 組件設計器生成的代碼 25 26 /// <summary> 27 /// 設計器支持所需的方法 - 不要 28 /// 使用代碼編輯器修改此方法的內容。 29 /// </summary> 30 private void InitializeComponent() 31 { 32 components = new System.ComponentModel.Container(); 33 // this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 34 SetStyle(ControlStyles.UserPaint, true); 35 SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景. 36 SetStyle(ControlStyles.DoubleBuffer, true); // 雙緩沖 37 SetStyle(ControlStyles.OptimizedDoubleBuffer, true);//解決閃爍 38 UpdateStyles(); 39 } 40 41 #endregion 42 } 43 }
在生成解決方案之后,需要在窗體應用程序項目中引用控件庫的項目,會在窗體設計器的工具箱中看見自己寫的這個圖片按鈕控件,如下圖
最后把這個控件拖到自己窗體上即可,再調試、運行並觀察結果,效果動態圖在引言部分