最近有朋友問到在winform程序上要做換膚功能的話,該如何處理,剛好前一段時間在項目中主導了程序換膚的這個功能.那就借這個機會整理一下,以分享給有需要的朋友.
1. 在winform程序上換膚,需要處理的涉及到每個控件及窗體.熟悉前端的朋友應該知道,在網頁上實現換膚主要通過在每個元素上定義指定的標識符(如class,id等特性),然后頁面通過加載不同的樣式文件去渲染不同的皮膚效果,其實在winform程序中實現的思想應該是一致.
2.如上描述,我們可能需要定制使用到的每個控件,以便能讀取指定的樣式,並能根據樣式渲染效果.
3.描述樣式特征,我們可主要從以下幾個方面考慮: 字體,顏色,圖片,邊框(當然延伸一下應該有對應的各種事件效果支持).
4.作為擴展,我們可能還希望樣式可以在外面靈活的配置樣式.
當然,市面上已經有很多成熟的winform皮膚組件產品,這類用於處理標准的后台管理類軟件是已經很足夠了,各位如果有類似需求也比較推薦這種形式.只不過我們的項目有些特殊(觸摸屏),里面大部分的功能不能使用原生態的控件得以完成.如下面的展示效果.各位,看到這里,如果覺得不合胃口,請繞道,有興趣的再往下看.
- 簡單分析一下這個的實現.
- 在上面的分析中,我們大致明白完成這個功能需要有一個承載控件展示效果的樣式集合,以及各個控件根據自己對應的主題樣式分別渲染.在這里,姑且我們將這里的樣式管理者定義為ApplicationStyle, 它負責對外提供某個主題下各個控件樣式的定義以及作為每個具體主題的基類.基於此,我們得到了類似如下的UML草圖.

- 基於以上的分析,簡單看一下這個ApplicationStyle實現.
/// <summary> /// 應用程序樣式 /// </summary> public abstract class ApplicationStyle { private static string _currentSkinName; //當前主題名稱 private static ApplicationStyle _current; //緩存當前樣式 private static object sync = new object(); //單例鎖 /// <summary> /// 默認主題名稱 /// </summary> protected static readonly string DefaultSkinName = "EnergyYellowApplicationStyle"; /// <summary> /// 皮膚名稱 Tag /// </summary> protected static string SkinNameTag { get; set; } /// <summary> /// 獲取或設置當前主題名稱 /// </summary> public static string CurrentSkinName { get { if (string.IsNullOrWhiteSpace(_currentSkinName)) { _currentSkinName = DefaultSkinName; } return _currentSkinName; } set { if (!string.IsNullOrWhiteSpace(value) && !string.Equals(value, _currentSkinName)) { //如果為自定義皮膚 if (value.StartsWith("CustomApplicationStyle|", StringComparison.CurrentCultureIgnoreCase) && value.Length > "CustomApplicationStyle|".Length) { _currentSkinName = "CustomApplicationStyle"; SkinNameTag = value.Substring("CustomApplicationStyle|".Length); //判斷自定義文件是否存在 var cusSkinFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Skins", SkinNameTag, "Skin.skn"); if (!File.Exists(cusSkinFile)) { _currentSkinName = DefaultSkinName; } } else { _currentSkinName = value; } var temp = Current; //臨時加載皮膚 } } } /// <summary> /// 獲取當前正在使用的樣式主題 /// </summary> public static ApplicationStyle Current { get { if (_current == null) { lock (sync) { if (_current == null) { _current = LoadCurrentStyle(); } } } return _current; } } /// <summary> /// 皮膚標題 /// </summary> public abstract string SkinTitle { get; } /// <summary> /// 皮膚名稱 /// </summary> public abstract string SkinName { get; } /// <summary> /// 主題顏色 /// </summary> public Color MainThemeColor { get; protected set; } /// <summary> /// Grid 樣式集合 /// </summary> public List<GridStyle> GridStyles { get; protected set; } /// <summary> /// Pop 彈出類型樣式集合 /// </summary> public List<PopPanelStyle> PopPanelStyles { get; protected set; } /// <summary> /// 按鈕樣式集合 /// </summary> public List<ButtonStyle> ButtonStyles { get; protected set; } protected ApplicationStyle() { } /// <summary> /// 加載當前樣式 /// </summary> /// <returns></returns> private static ApplicationStyle LoadCurrentStyle() { ApplicationStyle temp = null; //通過反射實例化當前正在使用的主題樣式 try { var type = Type.GetType(string.Format("Skins.{0}", CurrentSkinName)); temp = Activator.CreateInstance(type) as ApplicationStyle; temp.InitStyles(); //初始化樣式 } catch { temp = new PeacockBlueApplicationStyle(); temp.InitStyles(); } if (temp == null) { temp = new PeacockBlueApplicationStyle(); temp.InitStyles(); } return temp; } /// <summary> /// 初始化樣式 /// </summary> public virtual void InitStyles() { try { InitOrderDishGridStyles(); } catch (Exception ex) { LogUtil.Error("初始化點菜界面已點列表樣式失敗", ex); } try { InitGridStyles(); } catch (Exception ex) { LogUtil.Error("初始化Grid樣式失敗", ex); } try { InitButtonStyles(); } catch (Exception ex) { LogUtil.Error("初始化Button樣式失敗", ex); } } #region 初始化樣式集合 protected abstract void InitGridStyles(); protected abstract void InitButtonStyles(); protected abstract void InitPopPanelStyles(); #endregion }
- 有了以上的基礎,我們來嘗試着改寫一個控件的渲染效果,這里以DataGridView控件為例.
/// <summary> /// ExDataGridView /// </summary> public class ExDataGridView : DataGridView { private BodyOrDialogRegionType _regionType = BodyOrDialogRegionType.None; private GridStyle _style; /// <summary> /// 應用主題樣式 /// </summary> private void ApplyStyle() { if (_regionType != RMS.Skins.ControlStyles.BodyOrDialogRegionType.None && _style != null) { this.ColumnHeadersDefaultCellStyle.BackColor = _style.Header.BackColor; this.ColumnHeadersDefaultCellStyle.ForeColor = _style.Header.ForeColor; this.ColumnHeadersDefaultCellStyle.Font = _style.Header.Font; this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.BackgroundColor = _style.BackColor; this.RowsDefaultCellStyle.BackColor = _style.Row.BackColor; this.RowsDefaultCellStyle.ForeColor = _style.Row.ForeColor; this.Font = _style.Row.Font; this.RowsDefaultCellStyle.SelectionBackColor = _style.Row.SelectedBackColor; this.RowsDefaultCellStyle.SelectionForeColor = _style.Row.SelectedForeColor; this.RowsDefaultCellStyle.Font = _style.Row.Font; this.RowTemplate.DefaultCellStyle.BackColor = _style.Row.BackColor; this.RowTemplate.DefaultCellStyle.ForeColor = _style.Row.ForeColor; this.RowTemplate.DefaultCellStyle.Font = _style.Row.Font; this.BorderColor = _style.BorderColor; this.GridColor = _style.GridColor; } } /// <summary> /// 設置或獲取控件所處區域 /// </summary> public BodyOrDialogRegionType RegionType { get { return _regionType; } set { _regionType = value; //加載當前區域所對應的樣式 _style = ApplicationStyle.Current.GridStyles.FirstOrDefault(t => t.RegionType == _regionType); ApplyStyle(); this.Invalidate(); } } /// <summary> /// 構造函數 /// </summary> public POSDataGridView() { this.EnableHeadersVisualStyles = false; this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.EnableResizing; this.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; this.ColumnHeadersHeight = 37; this.ShowRowErrors = false; this.RowHeadersVisible = false; this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); } /// <summary> /// 在展示布局的時候,重新應用樣式 /// </summary> /// <param name="e"></param> protected override void OnLayout(LayoutEventArgs e) { base.OnLayout(e); ApplyStyle(); } /// <summary> /// 邊框顏色 /// </summary> public Color BorderColor { get; set; } /// <summary> /// 處理繪制事件 /// </summary> /// <param name="e"></param> protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); //繪制邊框 using (var p = new Pen(BorderColor)) { e.Graphics.DrawRectangle(p, 0, 0, this.Width - 1, this.Height - 1); } } /// <summary> /// 處理單元格繪制事件,應用自定義樣式效果 /// </summary> /// <param name="e"></param> protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e) { base.OnCellPainting(e); //表頭 if (e.RowIndex == -1) { DrawCellLine(e, this.GridColor, DashStyle.Solid); } else { DrawCellLine(e, this.GridColor, DashStyle.Dot); } } /// <summary> /// 繪制表格邊框 /// </summary> /// <param name="e"></param> /// <param name="borderColor"></param> /// <param name="backgroundColor"></param> /// <param name="lineMode"></param> private void DrawCellLine(DataGridViewCellPaintingEventArgs e, Color borderColor, DashStyle lineStyle) { if (_style != null && _regionType != BodyOrDialogRegionType.None) { var backgroundColor = _style.Header.BackColor;// this.ColumnHeadersDefaultCellStyle.BackColor; if (e.RowIndex > -1) { backgroundColor = this.RowsDefaultCellStyle.BackColor; if (this.Rows[e.RowIndex].Selected) { backgroundColor = this.RowsDefaultCellStyle.SelectionBackColor; } } e.Graphics.FillRectangle(new SolidBrush(backgroundColor), e.CellBounds); e.PaintContent(e.CellBounds); var rect = e.CellBounds; rect.Offset(new Point(-1, -1)); var pen = new Pen(new SolidBrush(borderColor)); pen.DashStyle = lineStyle; e.Graphics.DrawLine(pen, rect.X, rect.Y + rect.Height, rect.X + rect.Width, rect.Y + rect.Height); e.Handled = true; } }
- 最后,我們僅需要在程序開始運行的時候,設置當前配置主題樣式名稱即可.如:ApplicationStyle.CurrentSkinName = Configs.SkinName;
后記, 在程序中,換膚是一個比較常見的功能,也有很多成熟的實現方案,本處僅提供一種方案供大家參考. 另外,在我們的UML圖里有一個自定義的主題CustomApplicationStyle對象,這個就不打算深入討論了,無非就是從指定的配置中讀取樣式主題需要的東西來組合成系統期望的樣式集合而已.


