--------------201507091034更新---------------
首先感謝猿友E204在回復中的反饋。
--------------201507082014原文(已更新)---------------
適用於:.net 2.0+的Winform項目
有損錄制+制圖的原因不可能原樣展示出真實效果,可至文章結尾下載Demo體驗。
總體來說,此消息框比較適合用在需要反饋大量消息文本的場合,用標准消息框的話,文本太多可能會使消息框超出屏幕大小,比如codeproject.com上這位老兄舉的例子,由於標准消息框不具備改變窗體大小的能力,將導致部分消息無法讓用戶看到。而就算沒有超出屏幕,一下子讓用戶面對那麼多消息文字,體驗也不地道。使用本消息框就可以解決此類問題,比如可以將扼要信息顯示在主消息區,將大量的明細消息(例如批量處理中的單項處理情況)、次要消息、異常信息等放置在詳細信息區,由用戶或IT支持人員自己去展開獲取這些信息。同時,在沒有附加消息的時候,你仍然可以像標准消息框一樣使用它,所以,如果你跟我一樣不會用到標准消息框的IWin32Window、MessageBoxOptions和Help相關參數的話,基本上你可以在整個項目中全程用此消息框替換掉標准消息框,別忘了相比標准消息框,它還具備了可縮放、NT6下有聲音、相對父窗體居中等額外能力。總言之,你值得擁有。至於如果你擔心性能問題,這個~我想這麼說,我對自己的代碼質量還是有點信心的。也希望能得大俠指出槽點,感激!
先看公開成員:
//靜態屬性 MessageBoxEx.EnableAnimate MessageBoxEx.EnableSound //靜態方法 MessageBoxEx.Show(string, string, string) MessageBoxEx.Show(string, string, string, MessageBoxButtons) MessageBoxEx.Show(string, string, string, MessageBoxButtons, MessageBoxIcon) MessageBoxEx.Show(string, string, string, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton) MessageBoxEx.Show(string, string, Exception) MessageBoxEx.Show(string, string, Exception, MessageBoxButtons) MessageBoxEx.Show(string, string, Exception, MessageBoxButtons, MessageBoxIcon) MessageBoxEx.Show(string, string, Exception, MessageBoxButtons, MessageBoxIcon, MessageBoxDefaultButton)
MessageBoxEx.Show("主消息", "標題", "附加消息", MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); MessageBoxEx.Show("主消息", "標題", ex, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
MessageBoxEx.Show("阿斯頓發"); MessageBoxEx.Show("阿斯頓發", "士大夫");
代碼不少,原因自然是有的,有興趣的童鞋請看後面的實現說明。另外,千萬不要認為代碼量跟性能有直接關系,有時候更多的代碼恰恰是為了提升性能而存在,有時候則是為了健壯性。
using System; using System.ComponentModel; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; namespace AhDung.WinForm { /// <summary> /// 可以攜帶詳細信息的消息框 /// </summary> public static class MessageBoxEx { //異常消息文本 private const string InvalidButtonExString = "按鈕參數不是有效的枚舉項!"; private const string InvalidIconExString = "圖標參數不是有效的枚舉項!"; private const string InvalidDfButtonExString = "默認按鈕參數不是有效的枚舉項!"; /// <summary> /// 是否啟用動畫效果 /// </summary> public static bool EnableAnimate { get; set; } /// <summary> /// 是否啟用聲音反饋 /// </summary> public static bool EnableSound { get; set; } //靜態構造 static MessageBoxEx() { //默認啟用動畫+聲音 EnableAnimate = true; EnableSound = true; } #region 公開方法 /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="attachMessage">附加消息</param> public static DialogResult Show(string message, string caption = null, string attachMessage = null) { return ShowCore(message, caption, attachMessage, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); } /*下面這仨弄成重載而不是可選方法是為了避免不必要的參數檢查*/ /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="attachMessage">附加消息</param> /// <param name="buttons">按鈕組合</param> public static DialogResult Show(string message, string caption, string attachMessage, MessageBoxButtons buttons) { if (!Enum.IsDefined(typeof(MessageBoxButtons), buttons)) { throw new InvalidEnumArgumentException(InvalidButtonExString); } return ShowCore(message, caption, attachMessage, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); } /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="attachMessage">附加消息</param> /// <param name="buttons">按鈕組合</param> /// <param name="icon">圖標</param> public static DialogResult Show(string message, string caption, string attachMessage, MessageBoxButtons buttons, MessageBoxIcon icon) { if (!Enum.IsDefined(typeof(MessageBoxButtons), buttons)) { throw new InvalidEnumArgumentException(InvalidButtonExString); } if (!Enum.IsDefined(typeof(MessageBoxIcon), icon)) { throw new InvalidEnumArgumentException(InvalidIconExString); } return ShowCore(message, caption, attachMessage, buttons, icon, MessageBoxDefaultButton.Button1); } /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="attachMessage">附加消息</param> /// <param name="buttons">按鈕組合</param> /// <param name="icon">圖標</param> /// <param name="defaultButton">默認按鈕</param> public static DialogResult Show(string message, string caption, string attachMessage, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) { if (!Enum.IsDefined(typeof(MessageBoxButtons), buttons)) { throw new InvalidEnumArgumentException(InvalidButtonExString); } if (!Enum.IsDefined(typeof(MessageBoxIcon), icon)) { throw new InvalidEnumArgumentException(InvalidIconExString); } if (!Enum.IsDefined(typeof(MessageBoxDefaultButton), defaultButton)) { throw new InvalidEnumArgumentException(InvalidDfButtonExString); } return ShowCore(message, caption, attachMessage, buttons, icon, defaultButton); } /********傳入異常的重載********/ /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="exception">異常實例</param> public static DialogResult Show(string message, string caption, Exception exception) { return Show(message, caption, exception == null ? string.Empty : exception.ToString()); } /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="exception">異常實例</param> /// <param name="buttons">按鈕組合</param> public static DialogResult Show(string message, string caption, Exception exception, MessageBoxButtons buttons) { return Show(message, caption, exception == null ? string.Empty : exception.ToString(), buttons); } /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="exception">異常實例</param> /// <param name="buttons">按鈕組合</param> /// <param name="icon">圖標</param> public static DialogResult Show(string message, string caption, Exception exception, MessageBoxButtons buttons, MessageBoxIcon icon) { return Show(message, caption, exception == null ? string.Empty : exception.ToString(), buttons, icon); } /// <summary> /// 顯示消息框 /// </summary> /// <param name="message">消息文本</param> /// <param name="caption">消息框標題</param> /// <param name="exception">異常實例</param> /// <param name="buttons">按鈕組合</param> /// <param name="icon">圖標</param> /// <param name="defaultButton">默認按鈕</param> public static DialogResult Show(string message, string caption, Exception exception, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) { return Show(message, caption, exception == null ? string.Empty : exception.ToString(), buttons, icon, defaultButton); } #endregion //內部方法,不檢查參數有效性 private static DialogResult ShowCore(string message, string caption, string attachMessage, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) { using (MessageForm f = new MessageForm(message, caption, buttons, icon, defaultButton, attachMessage, EnableAnimate, EnableSound)) { return f.ShowDialog(); } } /*---------------- 下面是消息窗體相關 ---------------*/ /// <summary> /// 消息窗體 /// </summary> /// <remarks>參數有效性由MessageBoxEx負責</remarks> private class MessageForm : Form { /* todo 存在問題: * 當消息區文本非常非常多時,且反復進行改變消息框窗口大小、位置、展開收起的操作,那麼在某次展開時 詳細信息文本框可能會在原位置(即消息區內某rect)瞬閃一下, 原因是文本框控件在顯示時總會在原位置WM_NCPAINT + WM_ERASEBKGND一下,暫無解決辦法。 實際應用中碰到的幾率很小,就算碰到,影響也可以忽略。 */ #region 控件初始化 /// <summary> /// 必需的設計器變量。 /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// 清理所有正在使用的資源。 /// </summary> /// <param name="disposing">如果應釋放托管資源,為 true;否則為 false。</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows 窗體設計器生成的代碼 /// <summary> /// 設計器支持所需的方法 - 不要 /// 使用代碼編輯器修改此方法的內容。 /// </summary> private void InitializeComponent() { this.button3 = new System.Windows.Forms.Button(); this.txbAttach = new TextBoxUnSelectAllable(); this.button2 = new System.Windows.Forms.Button(); this.button1 = new System.Windows.Forms.Button(); this.plButtonsZone = new AhDung.WinForm.MessageBoxEx.MessageForm.PanelBasic(); this.ckbToggle = new AhDung.WinForm.MessageBoxEx.MessageForm.ToggleButton(this.UseAnimate); this.plAttachZone = new AhDung.WinForm.MessageBoxEx.MessageForm.PanelBasic(); this.lbMsg = new AhDung.WinForm.MessageBoxEx.MessageForm.MessageViewer(); this.plButtonsZone.SuspendLayout(); this.plAttachZone.SuspendLayout(); this.SuspendLayout(); // // button3 // this.button3.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; this.button3.Location = new System.Drawing.Point(320, 8); this.button3.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.button3.Name = "button3"; this.button3.Size = new System.Drawing.Size(85, 27); this.button3.TabIndex = 2; // // txbAttach // this.txbAttach.Anchor = ((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right; this.txbAttach.Location = new System.Drawing.Point(10, 7); this.txbAttach.Margin = new System.Windows.Forms.Padding(3, 1, 3, 1); this.txbAttach.Name = "txbAttach"; this.txbAttach.ReadOnly = true; this.txbAttach.Multiline = true; this.txbAttach.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; this.txbAttach.Size = new System.Drawing.Size(395, 105); this.txbAttach.TabIndex = 0; // // button2 // this.button2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; this.button2.Location = new System.Drawing.Point(229, 8); this.button2.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.button2.Name = "button2"; this.button2.Size = new System.Drawing.Size(85, 27); this.button2.TabIndex = 1; // // button1 // this.button1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; this.button1.Location = new System.Drawing.Point(138, 8); this.button1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(85, 27); this.button1.TabIndex = 0; // // plButtonsZone // this.plButtonsZone.Controls.Add(this.ckbToggle); this.plButtonsZone.Controls.Add(this.button1); this.plButtonsZone.Controls.Add(this.button2); this.plButtonsZone.Controls.Add(this.button3); this.plButtonsZone.Dock = System.Windows.Forms.DockStyle.Bottom; this.plButtonsZone.Location = new System.Drawing.Point(0, 96); this.plButtonsZone.Margin = new System.Windows.Forms.Padding(3, 1, 3, 1); this.plButtonsZone.Name = "plButtonsZone"; this.plButtonsZone.Size = new System.Drawing.Size(415, 36); this.plButtonsZone.TabIndex = 1; // // ckbToggle // this.ckbToggle.Location = new System.Drawing.Point(10, 8); this.ckbToggle.Name = "ckbToggle"; this.ckbToggle.Size = new System.Drawing.Size(93, 27); this.ckbToggle.TabIndex = 3; this.ckbToggle.Text = "詳細信息(&D)"; this.ckbToggle.CheckedChanged += this.ckbToggle_CheckedChanged; // // plAttachZone // this.plAttachZone.Controls.Add(this.txbAttach); this.plAttachZone.Dock = System.Windows.Forms.DockStyle.Fill; this.plAttachZone.Location = new System.Drawing.Point(0, 130); this.plAttachZone.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.plAttachZone.Name = "plAttachZone"; this.plAttachZone.Size = new System.Drawing.Size(415, 114); this.plAttachZone.TabIndex = 2; this.plAttachZone.Visible = false; // // lbMsg // this.lbMsg.Dock = System.Windows.Forms.DockStyle.Fill; this.lbMsg.Icon = null; this.lbMsg.Location = new System.Drawing.Point(0, 0); this.lbMsg.Name = "lbMsg"; this.lbMsg.Padding = new System.Windows.Forms.Padding(21, 18, 21, 18); //this.lbMsg.Size = new System.Drawing.Size(415, 96); this.lbMsg.TabIndex = 0; // // FmMsg // this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; //this.ClientSize = new System.Drawing.Size(415, 261); this.Controls.Add(this.lbMsg); this.Controls.Add(this.plButtonsZone); this.Controls.Add(this.plAttachZone); this.DoubleBuffered = true; this.MaximizeBox = false; this.Name = "MessageForm"; this.Padding = new System.Windows.Forms.Padding(0, 0, 0, 17); this.ShowIcon = false; this.ShowInTaskbar = false; this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show; this.plButtonsZone.ResumeLayout(false); this.plAttachZone.ResumeLayout(false); this.plAttachZone.PerformLayout(); this.ResumeLayout(false); } #endregion private ToggleButton ckbToggle; private TextBoxUnSelectAllable txbAttach; private MessageViewer lbMsg; private System.Windows.Forms.Button button2; private System.Windows.Forms.Button button1; private PanelBasic plButtonsZone; private PanelBasic plAttachZone; private System.Windows.Forms.Button button3; #endregion /// <summary> /// 最大默認窗體客戶區寬度 /// </summary> const int MaxClientWidth = 700; int expandHeight; /// <summary> /// 詳細信息區展開高度 /// </summary> private int ExpandHeight { get { return expandHeight < 150 ? 150 : expandHeight; } set { expandHeight = value; } } #region 屬性 /// <summary> /// 是否啟用動畫效果 /// </summary> /// <remarks>此處還弄該屬性是為了保證窗體類的獨立性</remarks> private bool UseAnimate { get; set; } /// <summary> /// 是否啟用聲音反饋 /// </summary> /// <remarks>此處還弄該屬性是為了保證窗體類的獨立性</remarks> private bool UseSound { get; set; } /// <summary> /// 消息按鈕 /// </summary> private MessageBoxButtons MessageButtons { get; set; } /// <summary> /// 消息圖標 /// </summary> private MessageBoxIcon MessageIcon { get; set; } /// <summary> /// 默認按鈕 /// </summary> private MessageBoxDefaultButton DefaultButton { get; set; } #endregion /// <summary> /// 創建消息窗體 /// </summary> private MessageForm(bool enableAnimate) { this.UseAnimate = enableAnimate;//須盡早設置,要供展開按鈕初始化用 InitializeComponent(); this.StartPosition = Form.ActiveForm == null ? FormStartPosition.CenterScreen : FormStartPosition.CenterParent; this.Font = SystemFonts.MessageBoxFont; //注冊事件 this.button1.Click += button_Click; this.button2.Click += button_Click; this.button3.Click += button_Click; this.plAttachZone.Resize += plAttachZone_Resize; } /// <summary> /// 創建消息窗體 /// </summary> public MessageForm(string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, string attachMessage, bool enableAnimate, bool enableSound) : this(enableAnimate) { this.lbMsg.Text = message; this.Text = caption; this.txbAttach.Text = attachMessage; this.MessageButtons = buttons; this.MessageIcon = icon; this.DefaultButton = defaultButton; this.UseSound = enableSound; } #region 重寫基類方法 protected override void OnLoad(EventArgs e) { //須在計算各種尺寸前搞掂 ProcessIcon(); ProcessButtons(); this.MinimumSize = SizeFromClientSize(new Size(GetPanelButtonMinWidth(), GetClientMinHeight())); //參數意義定為客戶區最大大小,所以需刨掉非客戶區高度後傳入 this.ClientSize = this.GetPreferredSize(new Size(MaxClientWidth, Screen.PrimaryScreen.WorkingArea.Height - (this.Height - this.ClientSize.Height))); base.OnLoad(e); } protected override void OnShown(EventArgs e) { //設置默認按鈕焦點。須在OnShown中設置按鈕焦點才有用 Button dfBtn; if ((dfBtn = this.AcceptButton as Button) != null) { dfBtn.Focus(); } //播放消息提示音 if (this.UseSound) { PlayMessageSound(this.MessageIcon); } base.OnShown(e); } //重寫窗體參數 protected override CreateParams CreateParams { get { CreateParams prms = base.CreateParams; if ((Convert.ToInt32(this.MessageButtons) & 1) == 0) //沒有Cancel按鈕時屏蔽關閉按鈕,剛好在偶數項 { prms.ClassStyle |= 0x200; } return prms; } } /// <summary> /// 計算合適的窗口尺寸 /// </summary> /// <param name="proposedSize">該參數此處定義為客戶區可設置的最大尺寸</param> public override Size GetPreferredSize(Size proposedSize) { int reservedHeight = plButtonsZone.Height + Padding.Bottom; Size size = lbMsg.GetPreferredSize(new Size(proposedSize.Width, proposedSize.Height - reservedHeight)); size.Height += reservedHeight; return size; } #endregion #region 事件處理方法 //展開收起 private void ckbToggle_CheckedChanged(object sender, EventArgs e) { this.SuspendLayout(); if (ckbToggle.Checked) { plButtonsZone.SendToBack(); lbMsg.SendToBack(); lbMsg.Dock = DockStyle.Top; plButtonsZone.Dock = DockStyle.Top; ChangeFormHeight(ExpandHeight); plAttachZone.Visible = true; } else { ExpandHeight = plAttachZone.Height;//為再次展開記憶高度 plAttachZone.Visible = false; ChangeFormHeight(-plAttachZone.Height);//收起時直接取pl高度,不要取ExpandHeight plButtonsZone.SendToBack(); plButtonsZone.Dock = DockStyle.Bottom; lbMsg.Dock = DockStyle.Fill; } this.ResumeLayout(); } //按鈕事件 private void button_Click(object sender, EventArgs e) { this.DialogResult = (DialogResult)((sender as Button).Tag); } //用戶手工收完詳細區則觸發折疊 private void plAttachZone_Resize(object sender, EventArgs e) { if (ckbToggle.Checked && plAttachZone.Height == 0) { ckbToggle.Checked = false; } } #endregion #region 輔助+私有方法 /// <summary> /// 處理按鈕相關 /// </summary> private void ProcessButtons() { this.ckbToggle.Visible = txbAttach.Text.Trim().Length != 0; //無詳細信息就不顯示展開按鈕 int btnCount = 3; //按鈕數量 switch (MessageButtons) //老實用case,可讀點 { case MessageBoxButtons.AbortRetryIgnore: button1.Text = "中止(&A)"; button1.Tag = DialogResult.Abort; button2.Text = "重試(&R)"; button2.Tag = DialogResult.Retry; button3.Text = "忽略(&I)"; button3.Tag = DialogResult.Ignore; break; case MessageBoxButtons.OK: button1.Visible = false; button2.Visible = false; button3.Text = "確定"; button3.Tag = DialogResult.OK; btnCount = 1; break; case MessageBoxButtons.OKCancel: button1.Visible = false; button2.Text = "確定"; button2.Tag = DialogResult.OK; button3.Text = "取消"; button3.Tag = DialogResult.Cancel; btnCount = 2; break; case MessageBoxButtons.RetryCancel: button1.Visible = false; button2.Text = "重試(&R)"; button2.Tag = DialogResult.Retry; button3.Text = "取消"; button3.Tag = DialogResult.Cancel; btnCount = 2; break; case MessageBoxButtons.YesNo: button1.Visible = false; button2.Text = "是(&Y)"; button2.Tag = DialogResult.Yes; button3.Text = "否(&N)"; button3.Tag = DialogResult.No; btnCount = 2; break; case MessageBoxButtons.YesNoCancel: button1.Text = "是(&Y)"; button1.Tag = DialogResult.Yes; button2.Text = "否(&N)"; button2.Tag = DialogResult.No; button3.Text = "取消"; button3.Tag = DialogResult.Cancel; break; default: break; } //僅有OK和有取消按鈕時設CancelButton if ((int)MessageButtons == 0 || ((int)MessageButtons & 1) == 1) { this.CancelButton = button3; } //處理默認按鈕 if (btnCount == 1) { this.AcceptButton = button3; } else if (btnCount == 2) { this.AcceptButton = DefaultButton == MessageBoxDefaultButton.Button2 ? button3 : button2; } else { Button[] btnArray = { button1, button2, button3 }; this.AcceptButton = btnArray[Convert.ToInt32(DefaultButton) / 0x100]; } } /// <summary> /// 處理圖標 /// </summary> /// <remarks>之所以不在此處順便把Sound處理了是為了松耦合</remarks> private void ProcessIcon() { switch (MessageIcon) { //MessageBoxIcon.Information同樣 case MessageBoxIcon.Asterisk: lbMsg.Icon = SystemIcons.Information; break; //MessageBoxIcon.Hand、MessageBoxIcon.Stop同樣 case MessageBoxIcon.Error: lbMsg.Icon = SystemIcons.Error; break; //MessageBoxIcon.Warning同樣 case MessageBoxIcon.Exclamation: lbMsg.Icon = SystemIcons.Warning; break; case MessageBoxIcon.Question: lbMsg.Icon = SystemIcons.Question; break; default: lbMsg.Icon = null; break; } } /// <summary> /// 計算窗體客戶區最小高度 /// </summary> private int GetClientMinHeight() { return lbMsg.MinimumHeight + plButtonsZone.Height + Padding.Bottom; } /// <summary> /// 計算按鈕區最小寬度 /// </summary> private int GetPanelButtonMinWidth() { int r = 20 /*左右Padding*/, visibleCount = -1 /*因為兩個以上才會有間距*/; if (ckbToggle.Visible) { r += ckbToggle.Width; visibleCount++; } if (button1.Visible) { r += button1.Width * 3; visibleCount += 3; } else if (button2.Visible) { r += button2.Width * 2; visibleCount += 2; } else { r += button3.Width; visibleCount++; } if (visibleCount != -1) { r += visibleCount * 6; } //按鈕間距 return r; } /// <summary> /// 改變窗體高度。內部有動畫處理 /// </summary> /// <param name="increment">增量(負數即為減小高度)</param> private void ChangeFormHeight(int increment) { int finalHeight = this.Height + increment; //正確的目標高度 if (!this.UseAnimate) //不使用動畫 { this.Height = finalHeight; return; } const int step = 8; //幀數 for (int i = 0; i < step; i++) { if (i == step - 1) //最後一步直達目標 { this.Height = finalHeight; return; } this.Height += increment / step; Application.DoEvents(); //必要 Thread.Sleep(10); } } /// <summary> /// 播放系統事件聲音 /// </summary> /// <remarks>之所以不用MessageBeep API是因為這貨在NT6上不出聲,所以用PlaySound代替</remarks> private static void PlayMessageSound(MessageBoxIcon msgType) { string eventString; switch (msgType) { case MessageBoxIcon.None: eventString = "SystemDefault"; break; //Question原本是沒聲音的,此實現讓它蹭一下Information的 case MessageBoxIcon.Question: //MessageBoxIcon.Information同樣 case MessageBoxIcon.Asterisk: eventString = "SystemAsterisk"; break; //MessageBoxIcon.Hand、MessageBoxIcon.Stop同樣 case MessageBoxIcon.Error: eventString = "SystemHand"; break; //MessageBoxIcon.Warning同樣 case MessageBoxIcon.Exclamation: eventString = "SystemExclamation"; break; default: throw new ArgumentOutOfRangeException(); } PlaySound(eventString, IntPtr.Zero, 0x10000 /*SND_ALIAS*/| 0x1 /*SND_ASYNC*/); } [DllImport("winmm.dll", CharSet = CharSet.Auto)] private static extern bool PlaySound([MarshalAs(UnmanagedType.LPWStr)] string soundName, IntPtr hmod, int soundFlags); #endregion #region 嵌套類 /// <summary> /// 基礎面板 /// </summary> private class PanelBasic : Control { public PanelBasic() { SetStyle(ControlStyles.AllPaintingInWmPaint, false);//關鍵,不要設置雙緩沖,不然其上的ToolBar不正常 SetStyle(ControlStyles.OptimizedDoubleBuffer, true);//重要。不設置的話控件繪制不正常 SetStyle(ControlStyles.ContainerControl, true); SetStyle(ControlStyles.Selectable, false); } protected override void WndProc(ref Message m) { //屏蔽WM_ERASEBKGND。防止顯示時在原位置快閃 //不能通過ControlStyles.AllPaintingInWmPaint=true屏蔽 //會影響其上的ToolBar if (m.Msg == 0x14) { return; } base.WndProc(ref m); } protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { //防Dock時面板短暫滯留在原位置 base.SetBoundsCore(x, y, width, height, specified | BoundsSpecified.Y | BoundsSpecified.Width); } } /// <summary> /// 消息呈現控件 /// </summary> private class MessageViewer : Control { const TextFormatFlags textFlags = TextFormatFlags.EndEllipsis //未完省略號 | TextFormatFlags.WordBreak //允許換行 | TextFormatFlags.NoPadding //無邊距 | TextFormatFlags.ExternalLeading //行間空白。NT5必須,不然文字擠在一起 | TextFormatFlags.TextBoxControl; //避免半行 const int IconSpace = 5; //圖標與文本間距 const float PreferredScale = 13;//最佳文本區塊比例(寬/高) /// <summary> /// 最小高度。不要重寫MinimumSize,那會在窗體移動和縮放時都會執行 /// </summary> public int MinimumHeight { get { return (this.Icon != null ? Math.Max(this.Icon.Height, this.Font.Height) : this.Font.Height) + Padding.Vertical; } } /// <summary> /// 獲取或設置圖標 /// </summary> public Icon Icon { get; set; } public MessageViewer() { this.SetStyle(ControlStyles.CacheText, true); this.SetStyle(ControlStyles.UserPaint, true); this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); this.SetStyle(ControlStyles.Selectable, false); this.SetStyle(ControlStyles.ResizeRedraw, true); //重要 this.DoubleBuffered = true; //雙緩沖 BackColor = Environment.OSVersion.Version.Major == 5 ? SystemColors.Control : Color.White; } //防Dock改變尺寸 protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { base.SetBoundsCore(x, y, width, height, specified | BoundsSpecified.Size); } /// <summary> /// 計算合適的消息區尺寸 /// </summary> /// <param name="proposedSize">該參數此處定義為此控件可設置的最大尺寸</param> /// <remarks>該方法對太長的單行文本有做比例優化處理,避免用戶擺頭幅度過大扭到脖子</remarks> public override Size GetPreferredSize(Size proposedSize) { if (proposedSize.Width < 10) { proposedSize.Width = int.MaxValue; } if (proposedSize.Height < 10) { proposedSize.Height = int.MaxValue; } int reservedWidth = Padding.Horizontal + (this.Icon == null ? 0 : (this.Icon.Width + IconSpace)); Size wellSize = Size.Empty; if (!string.IsNullOrEmpty(this.Text)) { //用指定寬度測量文本面積 Size size = TextRenderer.MeasureText(this.Text, this.Font, new Size(proposedSize.Width - reservedWidth, 0), textFlags); int lineHeight = TextRenderer.MeasureText(" ", this.Font, new Size(int.MaxValue, 0), textFlags).Height;//單行高,Font.Height不靠譜 wellSize = Convert.ToSingle(size.Width) / size.Height > PreferredScale //過於寬扁的情況 ? Size.Ceiling(GetSameSizeWithNewScale(size, PreferredScale)) : size; //湊齊整行高,確保尾行顯示 wellSize.Height = Convert.ToInt32(Math.Ceiling(wellSize.Height / Convert.ToDouble(lineHeight))) * lineHeight; } if (this.Icon != null) { wellSize.Width += this.Icon.Width + IconSpace; wellSize.Height = Math.Max(this.Icon.Height, wellSize.Height); } wellSize += Padding.Size; //不應超過指定尺寸。寬度在上面已確保不會超過 if (wellSize.Height > proposedSize.Height) { wellSize.Height = proposedSize.Height; } return wellSize; } /// <summary> /// 重繪 /// </summary> protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; Rectangle rect = GetPaddedRectangle(); //繪制圖標 if (this.Icon != null) { g.DrawIcon(this.Icon, Padding.Left, Padding.Top); //右移文本區 rect.X += this.Icon.Width + IconSpace; rect.Width -= this.Icon.Width + IconSpace; //若文字太少,則與圖標垂直居中 if (this.Text.Length < 100) { Size textSize = TextRenderer.MeasureText(g, this.Text, this.Font, rect.Size, textFlags); if (textSize.Height <= this.Icon.Height) { rect.Y += (this.Icon.Height - textSize.Height) / 2; } } } //g.FillRectangle(Brushes.Gainsboro, rect);//test //繪制文本 TextRenderer.DrawText(g, this.Text, this.Font, rect, Color.Black, textFlags); base.OnPaint(e); } /// <summary> /// 根據原尺寸,得到相同面積、且指定比例的新尺寸 /// </summary> /// <param name="src">原尺寸</param> /// <param name="scale">新尺寸比例。需是width/height</param> private static SizeF GetSameSizeWithNewScale(Size src, float scale) { int sqr = src.Width * src.Height;//原面積 double w = Math.Sqrt(sqr * scale);//新面積寬 return new SizeF(Convert.ToSingle(w), Convert.ToSingle(sqr / w)); } /// <summary> /// 獲取刨去Padding的內容區 /// </summary> private Rectangle GetPaddedRectangle() { Rectangle r = this.ClientRectangle; r.X += this.Padding.Left; r.Y += this.Padding.Top; r.Width -= this.Padding.Horizontal; r.Height -= this.Padding.Vertical; return r; } } /// <summary> /// 屏蔽全選消息的文本框 /// </summary> private class TextBoxUnSelectAllable : TextBox { protected override void WndProc(ref Message m) { //EM_SETSEL if (m.Msg == 0xB1) { return; } base.WndProc(ref m); } } /// <summary> /// 包裝ToolBarButton為單一控件 /// </summary> private class ToggleButton : Control { /// <summary> /// 展開/收起圖標數據 /// </summary> const string ImgDataBase64 = @"iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAYAAAB3AH1ZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAA3NJREFUeNqklVlPFEEQx/8zPccue6gorMd6gBegeCAQD4w+oCx+AInx IB4EfTK8+g2MQUUTcBU8En0wmvigEkyMxgcTjRrUqHFVUBRQQaJGl2WPmbG6dzCLWUiESf7T0739 666urqqVDjVcxT9PAWkfqZKUY491ktpIzaRXGPv5L15J+dZIRx26dqAwf56c48+Cx+1CzDDR//13 /seevvx3HZ8OxmLxMzSvjhT5Z+Nx8UoKfHOu31e+qWwZPBkOMBkwTAvRuAE21QuvJwNz5s6U25++ rv365dtC+4SxifJsfeVWvsCJ2TOzqyo2FsHt1OBSFeiqTItIsOhHw7JgGBZM+s72TcOvX+GccHgw k7qttgHj5slOLNE0tXZNSQGYJEEhiDEJusLoW4ZMfZnGJVv0QmHhYuiaup+zE+W5Aftyc/xMURRh acJIKpowqDVhkhu5LCspiY6k0OIL5s9mdrCNyp9sDKL+6PExeW5AwOebigRNiiVMkoFIPIFwlLcG huIm4mRI3DRpAQg38oPMmD6Nuz4wGn+koRGH64/hxr1HuHjl2qg8D8JcZ4ZTRCtLSDjT1Ijz51rS 5lfVzj2o2rWXXCzDPcnNh3L5K5WntdHYdAqng6cwa/EK+AuK8SDUSx65gUAlxR1ZkcqLLDBpkJ+S R8yOvbXw+vx4GOoZsXlZyQqsK10pNlDpjlVZDPMs0FL55mATLl04C39+EWblFf3l2zs+w7jZii1b Kkfw3IDOcDiS5/G4yLjknQcCAbrPW3j8plvMWlu8XGwOsblMASYjFh3i3S4SS+W3Vddg++6apJ8t OwN4HHH/p+G5AW3f+gbyvB632DwGHigSyjdvpn4b9ElZWF9aJE6uMAanJsOlK3jdNcAXuE2y0vEQ rcXfyeCT0vPcES0funoNRTJpgixSRUQsLbapogIbVq8S47rKCORShQvbX7437NI6Km8Ol9sxeG7A i2g0Fnz2PAQ3TcjQGBw02UGWOqig8L7bweB1qCSFxHD3/nMMDkWDnJ0oP1yK6z529y1i8ovydaVL wXOaXxl3W7K4yKKykY/Rdq8dofe9d+x6jonyw6WYu+Pyj5/hzLedPcU61dDJLh1T3E4BRgYjCHV0 4/qdJ+bn/h+naW41KZpiwLh5Kc3fMS+vNXaRybVT7YMdcM2228d6/ov/I8AAPfkI7yO+mM8AAAAA SUVORK5CYII="; readonly bool isToggleMode; bool isChecked; bool useAnimate; readonly ImageList imgList; /// <summary> /// Checked改變後 /// </summary> public event EventHandler CheckedChanged; /// <summary> /// 使用動畫按鈕效果 /// </summary> private bool UseAnimate { get { return useAnimate; } set { if (useAnimate == value) { return; } useAnimate = value; if (IsHandleCreated) { this.CreateHandle(); } } } /// <summary> /// 獲取或設置按鈕是否處於按下狀態 /// </summary> [Description("獲取或設置按鈕是否處於按下狀態"), DefaultValue(false)] public bool Checked { get { if (IsHandleCreated) { //保證isChecked與實情吻合。TB_ISBUTTONCHECKED isChecked = Convert.ToBoolean(SendMessage(this.Handle, 0x40A, IntPtr.Zero, IntPtr.Zero).ToInt32()); } return isChecked; } set { if (isChecked == value || !isToggleMode) { return; } isChecked = value; if (IsHandleCreated) { //TB_CHECKBUTTON SendMessage(this.Handle, 0x402, IntPtr.Zero, new IntPtr(Convert.ToInt32(value))); } OnCheckedChanged(EventArgs.Empty); } } /// <summary> /// 創建ToolBarButtonControl /// </summary> public ToggleButton(bool useAnimate) { SetStyle(ControlStyles.UserPaint, false); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.ResizeRedraw, true); this.isToggleMode = true;//寫死好了,獨立版才提供設置 this.UseAnimate = useAnimate; //將圖標加入imageList imgList = new ImageList { ImageSize = new System.Drawing.Size(16, 16), ColorDepth = ColorDepth.Depth32Bit }; using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(ImgDataBase64))) { imgList.Images.AddStrip(Image.FromStream(ms)); } } /// <summary> /// 執行左鍵單擊 /// </summary> public void PerformClick() { SendMessage(this.Handle, 0x201, new IntPtr(0x1), IntPtr.Zero);//WM_LBUTTONDOWN Application.DoEvents(); SendMessage(this.Handle, 0x202, IntPtr.Zero, IntPtr.Zero); //WM_LBUTTONUP } protected override void WndProc(ref Message m) { //忽略鼠標雙擊消息,WM_LBUTTONDBLCLK if (m.Msg == 0x203) { return; } //有節操的響應鼠標動作 if ((m.Msg == 0x201 || m.Msg == 0x202) && (!this.Enabled || !this.Visible)) { return; } base.WndProc(ref m); } //創建ToolBar protected override CreateParams CreateParams { get { CreateParams prms = base.CreateParams; prms.ClassName = "ToolbarWindow32"; prms.Style = 0x40000000 | 0x10000000 //| 0x2000000 //WS_CLIPCHILDREN //| 0x8000 | 0x1 | 0x4 | 0x8 | 0x40 | 0x1000 //TBSTYLE_LIST,圖標文本橫排 ; if (UseAnimate) { prms.Style |= 0x800; }//TBSTYLE_FLAT。flat模式在NT6.x下,按鈕按下會有動畫效果 prms.ExStyle = 0; return prms; } } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); //設置imgList SendMessage(this.Handle, 0x430, IntPtr.Zero, imgList.Handle);//TB_SETIMAGELIST //准備添加按鈕 int btnStructSize = Marshal.SizeOf(typeof(TBBUTTON)); SendMessage(this.Handle, 0x41E, new IntPtr(btnStructSize), IntPtr.Zero);//TB_BUTTONSTRUCTSIZE,必須在添加按鈕前 //構建按鈕信息 TBBUTTON btnStruct = new TBBUTTON { //iBitmap = 0, //idCommand = 0, fsState = 0x4, //TBSTATE_ENABLED iString = SendMessage(this.Handle, 0x44D, 0, this.Text + '\0')//TB_ADDSTRING }; if (this.isToggleMode) { btnStruct.fsStyle = 0x2; }//BTNS_CHECK。作為切換按鈕時 IntPtr btnStructStart = IntPtr.Zero; try { btnStructStart = Marshal.AllocHGlobal(btnStructSize);//在非托管區創建一個指針 Marshal.StructureToPtr(btnStruct, btnStructStart, true);//把結構體塞到上述指針 //添加按鈕 SendMessage(this.Handle, 0x444, new IntPtr(1)/*按鈕數量*/, btnStructStart);//TB_ADDBUTTONS。從指針取按鈕信息 //設置按鈕尺寸剛好為ToolBar尺寸 AdjustButtonSize(); } finally { if (btnStructStart != IntPtr.Zero) { Marshal.FreeHGlobal(btnStructStart); } } } protected override bool ProcessCmdKey(ref Message m, Keys keyData) { //將空格和回車作為鼠標單擊處理 if (m.Msg == 0x100 && (keyData == Keys.Enter || keyData == Keys.Space)) { PerformClick(); return true; } return base.ProcessCmdKey(ref m, keyData); } /// <summary> /// 處理助記鍵 /// </summary> protected override bool ProcessMnemonic(char charCode) { if (IsMnemonic(charCode, this.Text)) { PerformClick(); return true; } return base.ProcessMnemonic(charCode); } protected override void OnClick(EventArgs e) { //忽略鼠標右鍵 MouseEventArgs me = e as MouseEventArgs; if (me != null && me.Button != System.Windows.Forms.MouseButtons.Left) { return; } //若是切換模式,直接引發Checked事件(不要通過設置Checked屬性引發,因為OnClick發送之前就已經Check了) //存在理論上的不可靠,但暫無更好辦法 if (isToggleMode) { this.OnCheckedChanged(EventArgs.Empty); } base.OnClick(e); } //重繪後重設按鈕尺寸 protected override void OnInvalidated(InvalidateEventArgs e) { base.OnInvalidated(e); AdjustButtonSize(); } /// <summary> /// 引發CheckedChanged事件 /// </summary> protected virtual void OnCheckedChanged(EventArgs e) { SetImageIndex(this.Checked ? 1 : 0); if (CheckedChanged != null) { CheckedChanged(this, e); } } /// <summary> /// 設置圖標索引 /// </summary> private void SetImageIndex(int index) { //TB_CHANGEBITMAP SendMessage(this.Handle, 0x42B, IntPtr.Zero, new IntPtr(index)); } /// <summary> /// 調整按鈕尺寸剛好為ToolBar尺寸 /// </summary> private void AdjustButtonSize() { IntPtr lParam = new IntPtr((this.Width & 0xFFFF) | (this.Height << 0x10)); //MakeLParam手法 SendMessage(this.Handle, 0x41F, IntPtr.Zero, lParam); //TB_SETBUTTONSIZE } #region Win32 API [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, string lParam); [StructLayout(LayoutKind.Sequential)] private struct TBBUTTON { public int iBitmap; public int idCommand; public byte fsState; public byte fsStyle; public byte bReserved0; public byte bReserved1; public IntPtr dwData; public IntPtr iString; } #endregion } #endregion } } } MessageBoxEx.cs以下內容獻給愛“八卦”和蛋疼的童鞋。這裡先貼個概要類圖,詳細的後面有完整Demo下載,你可以down回去慢慢研究。
說了這麼多,自以為很理想的實現,可能槽點也不少,再次懇請路過大俠指點,謝謝。
最後,Demo在此,裡面有個Tester供你體驗:
-文畢-