├── .tfignore ├── screenshots └── 2.0.png ├── Tester ├── Resources │ └── PicDemo.gif ├── Program.cs ├── Tester.csproj ├── FmMDI.cs ├── FmTester.cs ├── Properties │ ├── Resources.Designer.cs │ └── Resources.resx ├── FmMDI.resx ├── FmTester.resx ├── FmMDI.Designer.cs └── FmTester.Designer.cs ├── README.md ├── .gitignore ├── MessageTip ├── MessageTip.csproj ├── MessageTip │ ├── Border.cs │ ├── TipStyle.cs │ ├── GraphicsUtils.cs │ ├── MessageTip.cs │ └── LayeredWindow.cs └── MessageTip.v1.cs └── MessageTip.sln /.tfignore: -------------------------------------------------------------------------------- 1 | \.git -------------------------------------------------------------------------------- /screenshots/2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahdung/MessageTip/HEAD/screenshots/2.0.png -------------------------------------------------------------------------------- /Tester/Resources/PicDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahdung/MessageTip/HEAD/Tester/Resources/PicDemo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | MessageTip是一个轻快型消息提示窗,目前适用于.net framework 2.0+/.net core/.net的Winform项目,后面不排除会支持WPF。 3 | 4 | 默认样式方面,除良好消息会浮动外,其余消息均固定,因为实践下来感觉浮动会影响阅读,而除良好以外的其它消息恰恰是需要留意的,所以固定。当然,这是默认做法,你仍然可以控制。 5 | 6 | ## 用法 7 | 两种: 8 | 1. 拷贝MessageTip目录到您的项目 9 | 1. 编译成dll引用 10 | 11 | ## 截图 12 | ![image](screenshots/2.0.png) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | *.obj 3 | *.exe 4 | *.pdb 5 | *.user 6 | *.aps 7 | *.pch 8 | *.vspscc 9 | *_i.c 10 | *_p.c 11 | *.ncb 12 | *.suo 13 | *.sln.docstates 14 | *.tlb 15 | *.tlh 16 | *.bak 17 | *.cache 18 | *.ilk 19 | *.log 20 | [Bb]in 21 | [Dd]ebug*/ 22 | *.lib 23 | *.sbr 24 | obj/ 25 | [Rr]elease*/ 26 | _ReSharper*/ 27 | [Tt]est[Rr]esult* 28 | *.vssscc 29 | $tf*/ 30 | Fm[0-9].* 31 | .vs/ -------------------------------------------------------------------------------- /Tester/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using System; 4 | using System.Drawing; 5 | using System.Windows.Forms; 6 | 7 | namespace AhDung; 8 | 9 | static class Program 10 | { 11 | /// 12 | /// The main entry point for the application. 13 | /// 14 | [STAThread] 15 | static void Main() 16 | { 17 | // To customize application configuration such as set high DPI settings or default font, 18 | // see https://aka.ms/applicationconfiguration. 19 | #if NET 20 | //ApplicationConfiguration.Initialize(); 21 | Application.SetDefaultFont(SystemFonts.DefaultFont); 22 | #endif 23 | Application.EnableVisualStyles(); 24 | Application.SetCompatibleTextRenderingDefault(false); 25 | Application.Run(new FmMDI()); 26 | } 27 | } -------------------------------------------------------------------------------- /MessageTip/MessageTip.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | AhDung 7 | AhDung.$(MSBuildProjectName) 8 | net20;net40;net48;net6.0-windows 9 | latest 10 | false 11 | MessageTip 12 | 轻型消息窗 13 | AhDung 14 | MessageTip 15 | Copyright © AhDung 2016-2023 16 | 2.0.1.0 17 | True 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Tester/Tester.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net20;net40;net48;net6.0-windows 6 | true 7 | 8 | AhDung 9 | latest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Resources.resx 31 | True 32 | True 33 | 34 | 35 | 36 | 37 | 38 | Resources.Designer.cs 39 | ResXFileCodeGenerator 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Tester/FmMDI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Windows.Forms; 4 | 5 | namespace AhDung 6 | { 7 | public partial class FmMDI : Form 8 | { 9 | public FmMDI() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | void btnNewChild_Click(object sender, EventArgs e) 15 | { 16 | new FmTester 17 | { 18 | Text = "Form " + (MdiChildren.Length + 1), 19 | MdiParent = this 20 | }.Show(); 21 | } 22 | 23 | void btnNewForm_Click(object sender, EventArgs e) 24 | { 25 | new FmTester().Show(); 26 | } 27 | 28 | void btnTestItem_Click(object sender, EventArgs e) 29 | { 30 | MessageTip.ShowOk((ToolStripItem)sender, txbText.Text); 31 | } 32 | 33 | void txbText_KeyDown(object sender, KeyEventArgs e) 34 | { 35 | if (e.KeyCode == Keys.Enter) 36 | { 37 | btnShow.PerformClick(); 38 | } 39 | } 40 | 41 | void btnShow_Click(object sender, EventArgs e) 42 | { 43 | MessageTip.ShowOk(txbText.Text); 44 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 45 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 46 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 47 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 48 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 49 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 50 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 51 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 52 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 53 | //ThreadPool.QueueUserWorkItem(_ => MessageTip.ShowOk("并行测试")); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Tester/FmTester.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using System; 4 | using System.Windows.Forms; 5 | 6 | namespace AhDung; 7 | 8 | public sealed partial class FmTester : Form 9 | { 10 | TipStyle _style; 11 | 12 | public FmTester() 13 | { 14 | DoubleBuffered = true; 15 | InitializeComponent(); 16 | nudDelay.Value = MessageTip.Delay; 17 | nudFade.Value = MessageTip.Fade; 18 | _style = new TipStyle(); 19 | propertyGrid1.SelectedObject = _style; 20 | } 21 | 22 | void nudDelay_ValueChanged(object sender, EventArgs e) 23 | { 24 | MessageTip.Delay = decimal.ToInt32(nudDelay.Value); 25 | } 26 | 27 | void nudFade_ValueChanged(object sender, EventArgs e) 28 | { 29 | MessageTip.Fade = decimal.ToInt32(nudFade.Value); 30 | } 31 | 32 | void btnOk_Click(object sender, EventArgs e) 33 | { 34 | MessageTip.ShowOk(txbMultiline.Text); 35 | } 36 | 37 | void btnWarning_Click(object sender, EventArgs e) 38 | { 39 | MessageTip.ShowWarning(txbMultiline.Text); 40 | } 41 | 42 | void btnError_Click(object sender, EventArgs e) 43 | { 44 | MessageTip.ShowError(txbMultiline.Text); 45 | } 46 | 47 | void btnShow_Click(object sender, EventArgs e) 48 | { 49 | try 50 | { 51 | MessageTip.Show(txbMultiline.Text, _style); 52 | } 53 | catch (Exception ex) 54 | { 55 | MessageBox.Show(ex.Message); 56 | } 57 | } 58 | 59 | void btnShowInPanel_Click(object sender, EventArgs e) 60 | { 61 | MessageTip.Show(panel1, txbMultiline.Text); 62 | } 63 | 64 | void btnEnter_Click(object sender, EventArgs e) 65 | { 66 | MessageTip.Show((ToolStripItem)sender, txbMultiline.Text); 67 | } 68 | 69 | void btnRestore_Click(object sender, EventArgs e) 70 | { 71 | _style.Clear(); 72 | _style = new TipStyle(); 73 | propertyGrid1.SelectedObject = _style; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tester/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace AhDung.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AhDung.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找 System.Drawing.Bitmap 类型的本地化资源。 65 | /// 66 | internal static System.Drawing.Bitmap PicDemo { 67 | get { 68 | object obj = ResourceManager.GetObject("PicDemo", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MessageTip.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33927.249 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessageTip", "MessageTip\MessageTip.csproj", "{DDBDAC74-3561-454D-B38D-BE5E44E18C2E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{421A2147-5F83-4D55-A8B7-A0771069A2AF}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{6C4D8ACE-A5E9-4893-8FA9-F17D4A221BEA}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x64 = Debug|x64 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|x64 = Release|x64 22 | Release|x86 = Release|x86 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|x64.ActiveCfg = Debug|x64 28 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|x64.Build.0 = Debug|x64 29 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|x86.ActiveCfg = Debug|x86 30 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Debug|x86.Build.0 = Debug|x86 31 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Release|x64.ActiveCfg = Release|x64 34 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Release|x64.Build.0 = Release|x64 35 | {DDBDAC74-3561-454D-B38D-BE5E44E18C2E}.Release|x86.ActiveCfg = Release|Any CPU 36 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|x64.Build.0 = Debug|Any CPU 40 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Debug|x86.Build.0 = Debug|Any CPU 42 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|x64.ActiveCfg = Release|Any CPU 45 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|x64.Build.0 = Release|Any CPU 46 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|x86.ActiveCfg = Release|Any CPU 47 | {421A2147-5F83-4D55-A8B7-A0771069A2AF}.Release|x86.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {5895D95C-7280-47B7-A58B-7A4FA644FA7B} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /MessageTip/MessageTip/Border.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using System; 4 | using System.Drawing; 5 | using System.Drawing.Drawing2D; 6 | 7 | namespace AhDung.Drawing; 8 | 9 | /// 10 | /// 描边位置 11 | /// 12 | public enum Direction 13 | { 14 | /// 15 | /// 居中 16 | /// 17 | Middle, 18 | 19 | /// 20 | /// 内部 21 | /// 22 | Inner, 23 | 24 | /// 25 | /// 外部 26 | /// 27 | Outer 28 | } 29 | 30 | /// 31 | /// 存储一系列边框要素并产生合适的画笔 32 | /// - 边框居中+奇数粗度时是介于两像素之间画,所以粗细在视觉上不精确,建议错开任一条件 33 | /// 34 | public class Border : IDisposable 35 | { 36 | //编写本类除了整合边框信息外,更重要的原因是如果不对画笔做额外处理, 37 | //Draw出来的边框是不理想的。本类的原理是: 38 | // - 偶数边框(这是得到理想效果的前提) 39 | // - 再利用画笔的CompoundArray属性将边框裁切掉一半, 40 | // 同时根据不同参数偏移描边位置,达到可内可外可居中的效果 41 | 42 | float[] _compoundArray; 43 | 44 | /// 45 | /// 根据_direction处理线段 46 | /// 47 | float[] CompoundArray 48 | { 49 | get 50 | { 51 | _compoundArray ??= new float[2]; 52 | 53 | switch (_direction) 54 | { 55 | case Direction.Middle: goto default; 56 | case Direction.Inner: 57 | _compoundArray[0] = 0.5f; 58 | _compoundArray[1] = 1f; 59 | break; 60 | case Direction.Outer: 61 | _compoundArray[0] = 0f; 62 | _compoundArray[1] = 0.5f; 63 | break; 64 | default: 65 | _compoundArray[0] = 0.25f; 66 | _compoundArray[1] = 0.75f; 67 | break; 68 | } 69 | 70 | return _compoundArray; 71 | } 72 | } 73 | 74 | /// 75 | /// 获取用于画本边框的画笔。建议销毁本类而不是该画笔 76 | /// 77 | public Pen Pen { get; } 78 | 79 | /// 80 | /// 边框宽度。默认1 81 | /// 82 | public int Width 83 | { 84 | get => (int)Pen.Width / 2; 85 | set => Pen.Width = value * 2; 86 | } 87 | 88 | /// 89 | /// 边框颜色 90 | /// 91 | public Color Color 92 | { 93 | get => Pen.Color; 94 | set => Pen.Color = value; 95 | } 96 | 97 | Direction _direction; 98 | 99 | /// 100 | /// 边框位置。默认居中 101 | /// 102 | public Direction Direction 103 | { 104 | get => _direction; 105 | set 106 | { 107 | if (_direction == value) 108 | return; 109 | 110 | _direction = value; 111 | Pen.CompoundArray = CompoundArray; 112 | } 113 | } 114 | 115 | /// 116 | /// 描边是否躲在填充之后。默认false 117 | /// - 如果躲,则处于内部的部分会被填充遮挡,反之则是填充被这部分边框遮挡 118 | /// - 此属性仅供外部在绘制时确定描边和填充的次序 119 | /// 120 | public bool Behind { get; set; } 121 | 122 | /// 123 | /// 获取指定矩形加上本边框后的边界 124 | /// 125 | public Rectangle GetBounds(Rectangle rectangle) 126 | { 127 | if (!IsValid() || _direction == Direction.Inner) 128 | return rectangle; 129 | 130 | var inflate = _direction == Direction.Middle 131 | ? (int)Math.Ceiling(Width / 2d) 132 | : Width; 133 | 134 | rectangle.Inflate(inflate, inflate); 135 | return rectangle; 136 | } 137 | 138 | /// 139 | /// 指定颜色构造画笔 140 | /// 141 | public Border(Color color) : this(color, 1) 142 | { 143 | } 144 | 145 | /// 146 | /// 指定颜色和宽度构造画笔 147 | /// 148 | public Border(Color color, int width) : this(new Pen(color, width), false) 149 | { 150 | } 151 | 152 | /// 153 | /// 基于现有画笔的副本构造 154 | /// 155 | public Border(Pen pen) : this(pen, true) 156 | { 157 | } 158 | 159 | /// 160 | /// 基于现有画笔构造 161 | /// 162 | protected Border(Pen pen, bool useCopy) 163 | { 164 | Pen = useCopy ? (Pen)pen.Clone() : pen; 165 | Pen.Alignment = PenAlignment.Center; 166 | Pen.Width *= 2; 167 | Pen.CompoundArray = CompoundArray; 168 | } 169 | 170 | /// 171 | /// 是否有效边框。无宽度或完全透明视为无效 172 | /// 173 | public bool IsValid() => Width > 0 && (Pen.PenType != PenType.SolidColor || Color.A > 0); 174 | 175 | /// 176 | /// 是否有效边框。无宽度或完全透明视为无效 177 | /// 178 | public static bool IsValid(Border border) => border != null && border.IsValid(); 179 | 180 | /// 181 | /// 确定指定颜色和宽度能否构成有效边框。有效=有色+有宽度 182 | /// 183 | public static bool IsValid(Color color, int width) => width > 0 && color.A > 0; 184 | 185 | /// 186 | public void Dispose() => Pen?.Dispose(); 187 | } -------------------------------------------------------------------------------- /Tester/FmMDI.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 17, 17 122 | 123 | -------------------------------------------------------------------------------- /Tester/FmTester.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 17, 17 122 | 123 | -------------------------------------------------------------------------------- /Tester/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\PicDemo.gif;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | -------------------------------------------------------------------------------- /Tester/FmMDI.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace AhDung 2 | { 3 | partial class FmMDI 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.toolStrip1 = new System.Windows.Forms.ToolStrip(); 32 | this.btnNewChild = new System.Windows.Forms.ToolStripButton(); 33 | this.btnNewForm = new System.Windows.Forms.ToolStripButton(); 34 | this.btnTestItem = new System.Windows.Forms.ToolStripButton(); 35 | this.panel1 = new System.Windows.Forms.Panel(); 36 | this.btnShow = new System.Windows.Forms.Button(); 37 | this.txbText = new System.Windows.Forms.TextBox(); 38 | this.label1 = new System.Windows.Forms.Label(); 39 | this.toolStrip1.SuspendLayout(); 40 | this.panel1.SuspendLayout(); 41 | this.SuspendLayout(); 42 | // 43 | // toolStrip1 44 | // 45 | this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { 46 | this.btnNewChild, 47 | this.btnNewForm, 48 | this.btnTestItem}); 49 | this.toolStrip1.Location = new System.Drawing.Point(0, 0); 50 | this.toolStrip1.Name = "toolStrip1"; 51 | this.toolStrip1.Size = new System.Drawing.Size(856, 25); 52 | this.toolStrip1.TabIndex = 0; 53 | this.toolStrip1.Text = "toolStrip1"; 54 | // 55 | // btnNewChild 56 | // 57 | this.btnNewChild.Image = global::AhDung.Properties.Resources.PicDemo; 58 | this.btnNewChild.ImageTransparentColor = System.Drawing.Color.Magenta; 59 | this.btnNewChild.Name = "btnNewChild"; 60 | this.btnNewChild.Size = new System.Drawing.Size(117, 22); 61 | this.btnNewChild.Text = "New ChildForm"; 62 | this.btnNewChild.Click += new System.EventHandler(this.btnNewChild_Click); 63 | // 64 | // btnNewForm 65 | // 66 | this.btnNewForm.Image = global::AhDung.Properties.Resources.PicDemo; 67 | this.btnNewForm.ImageTransparentColor = System.Drawing.Color.Magenta; 68 | this.btnNewForm.Name = "btnNewForm"; 69 | this.btnNewForm.Size = new System.Drawing.Size(132, 22); 70 | this.btnNewForm.Text = "New NormalForm"; 71 | this.btnNewForm.Click += new System.EventHandler(this.btnNewForm_Click); 72 | // 73 | // btnTestItem 74 | // 75 | this.btnTestItem.Image = global::AhDung.Properties.Resources.PicDemo; 76 | this.btnTestItem.ImageTransparentColor = System.Drawing.Color.Magenta; 77 | this.btnTestItem.Name = "btnTestItem"; 78 | this.btnTestItem.Size = new System.Drawing.Size(177, 22); 79 | this.btnTestItem.Text = "MDI Parent ToolStripItem"; 80 | this.btnTestItem.Click += new System.EventHandler(this.btnTestItem_Click); 81 | // 82 | // panel1 83 | // 84 | this.panel1.Controls.Add(this.btnShow); 85 | this.panel1.Controls.Add(this.txbText); 86 | this.panel1.Controls.Add(this.label1); 87 | this.panel1.Dock = System.Windows.Forms.DockStyle.Top; 88 | this.panel1.Location = new System.Drawing.Point(0, 25); 89 | this.panel1.Name = "panel1"; 90 | this.panel1.Size = new System.Drawing.Size(856, 47); 91 | this.panel1.TabIndex = 1; 92 | // 93 | // btnShow 94 | // 95 | this.btnShow.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 96 | this.btnShow.Location = new System.Drawing.Point(769, 11); 97 | this.btnShow.Name = "btnShow"; 98 | this.btnShow.Size = new System.Drawing.Size(75, 23); 99 | this.btnShow.TabIndex = 1; 100 | this.btnShow.Text = "Show"; 101 | this.btnShow.UseVisualStyleBackColor = true; 102 | this.btnShow.Click += new System.EventHandler(this.btnShow_Click); 103 | // 104 | // txbText 105 | // 106 | this.txbText.AcceptsReturn = true; 107 | this.txbText.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 108 | | System.Windows.Forms.AnchorStyles.Right))); 109 | this.txbText.Location = new System.Drawing.Point(49, 13); 110 | this.txbText.Name = "txbText"; 111 | this.txbText.Size = new System.Drawing.Size(714, 21); 112 | this.txbText.TabIndex = 0; 113 | this.txbText.Text = "Try press Enter key"; 114 | this.txbText.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txbText_KeyDown); 115 | // 116 | // label1 117 | // 118 | this.label1.AutoSize = true; 119 | this.label1.Location = new System.Drawing.Point(14, 16); 120 | this.label1.Name = "label1"; 121 | this.label1.Size = new System.Drawing.Size(29, 12); 122 | this.label1.TabIndex = 0; 123 | this.label1.Text = "Text"; 124 | // 125 | // FmMDI 126 | // 127 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); 128 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 129 | this.ClientSize = new System.Drawing.Size(856, 654); 130 | this.Controls.Add(this.panel1); 131 | this.Controls.Add(this.toolStrip1); 132 | this.IsMdiContainer = true; 133 | this.Name = "FmMDI"; 134 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 135 | this.Text = "FmMDI"; 136 | this.toolStrip1.ResumeLayout(false); 137 | this.toolStrip1.PerformLayout(); 138 | this.panel1.ResumeLayout(false); 139 | this.panel1.PerformLayout(); 140 | this.ResumeLayout(false); 141 | this.PerformLayout(); 142 | 143 | } 144 | 145 | #endregion 146 | 147 | private System.Windows.Forms.ToolStrip toolStrip1; 148 | private System.Windows.Forms.ToolStripButton btnNewChild; 149 | private System.Windows.Forms.ToolStripButton btnNewForm; 150 | private System.Windows.Forms.ToolStripButton btnTestItem; 151 | private System.Windows.Forms.Panel panel1; 152 | private System.Windows.Forms.Button btnShow; 153 | private System.Windows.Forms.TextBox txbText; 154 | private System.Windows.Forms.Label label1; 155 | } 156 | } -------------------------------------------------------------------------------- /MessageTip/MessageTip/TipStyle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using System; 4 | using System.Drawing; 5 | using System.Drawing.Drawing2D; 6 | using System.Runtime.CompilerServices; 7 | using System.Windows.Forms; 8 | using AhDung.Drawing; 9 | 10 | namespace AhDung; 11 | 12 | /// 13 | /// 消息窗样式 14 | /// 15 | public sealed class TipStyle : IDisposable 16 | { 17 | bool _isPreset; 18 | bool _keepFont; 19 | bool _keepIcon; 20 | 21 | /// 22 | /// 获取边框信息。内部用 23 | /// 24 | internal Border Border { get; } 25 | 26 | /// 27 | /// 获取或设置图标。默认null 28 | /// 29 | public Bitmap Icon { get; set; } 30 | 31 | /// 32 | /// 获取或设置图标与文本的间距。默认为3像素 33 | /// 34 | public int IconSpacing { get; set; } 35 | 36 | /// 37 | /// 获取或设置文本字体。默认12号的消息框文本 38 | /// 39 | public Font TextFont { get; set; } 40 | 41 | /// 42 | /// 获取或设置文本偏移,用于微调 43 | /// 44 | public Point TextOffset { get; set; } 45 | 46 | /// 47 | /// 获取或设置文本颜色(默认黑色) 48 | /// 49 | public Color TextColor { get; set; } 50 | 51 | /// 52 | /// 获取或设置背景颜色(默认浅白) 53 | /// - 若想呈现多色及复杂背景,请使用BackBrush属性,当后者不为null时,本属性被忽略 54 | /// 55 | public Color BackColor { get; set; } 56 | 57 | /// 58 | /// 获取或设置背景画刷生成方法 59 | /// - 是个委托,入参矩形由绘制函数传入,表示内容区,便于构造画刷 60 | /// - 默认null,为null时使用BackColor绘制单色背景 61 | /// - 方法返回的画刷需释放 62 | /// 63 | public BrushCreator BackBrush { get; set; } 64 | 65 | /// 66 | /// 获取或设置边框颜色(默认深灰) 67 | /// 68 | public Color BorderColor 69 | { 70 | get => Border.Color; 71 | set => Border.Color = value; 72 | } 73 | 74 | /// 75 | /// 获取或设置边框粗细(默认1) 76 | /// 77 | public int BorderWidth 78 | { 79 | get => Border.Width / 2; 80 | set => Border.Width = value * 2; 81 | } 82 | 83 | /// 84 | /// 获取或设置圆角半径(默认3) 85 | /// 86 | public int CornerRadius { get; set; } 87 | 88 | /// 89 | /// 获取或设置阴影颜色(默认深灰) 90 | /// 91 | public Color ShadowColor { get; set; } 92 | 93 | /// 94 | /// 获取或设置阴影羽化半径(默认4) 95 | /// 96 | public int ShadowRadius { get; set; } 97 | 98 | /// 99 | /// 获取或设置阴影偏移(默认x=0,y=3) 100 | /// 101 | public Point ShadowOffset { get; set; } 102 | 103 | /// 104 | /// 获取或设置四周空白(默认left,right=10; top,bottom=5) 105 | /// 106 | public Padding Padding { get; set; } 107 | 108 | /// 109 | /// 初始化样式 110 | /// 111 | public TipStyle() 112 | { 113 | Border = new Border(PresetResources.Colors[0, 0]) 114 | { 115 | Behind = true, 116 | Width = 2 117 | }; 118 | IconSpacing = 5; 119 | TextFont = new Font(SystemFonts.MessageBoxFont.FontFamily, 12); 120 | var fontName = TextFont.Name; 121 | if (fontName == "宋体") 122 | TextOffset = new Point(1, 1); 123 | 124 | TextColor = Color.Black; 125 | BackColor = Color.FromArgb(252, 252, 252); 126 | CornerRadius = 3; 127 | ShadowColor = PresetResources.Colors[0, 2]; 128 | ShadowRadius = 4; 129 | ShadowOffset = new Point(0, 3); 130 | Padding = new Padding(10, 5, 10, 5); 131 | } 132 | 133 | /// 134 | /// 清理本类使用的资源 135 | /// 136 | /// 是否保留字体 137 | /// 是否保留图标 138 | public void Clear(bool keepFont = false, bool keepIcon = false) 139 | { 140 | _keepFont = keepFont; 141 | _keepIcon = keepIcon; 142 | ((IDisposable)this).Dispose(); 143 | } 144 | 145 | /// 146 | /// 预置的灰色样式 147 | /// 148 | public static TipStyle Gray { get; } = CreatePresetStyle(0); 149 | 150 | /// 151 | /// 预置的绿色样式 152 | /// 153 | public static TipStyle Green { get; } = CreatePresetStyle(1); 154 | 155 | /// 156 | /// 预置的橙色样式 157 | /// 158 | public static TipStyle Orange { get; } = CreatePresetStyle(2); 159 | 160 | /// 161 | /// 预置的红色样式 162 | /// 163 | public static TipStyle Red { get; } = CreatePresetStyle(3); 164 | 165 | static TipStyle CreatePresetStyle(int index) => new() 166 | { 167 | Icon = PresetResources.Icons[index], 168 | BorderColor = PresetResources.Colors[index, 0], 169 | ShadowColor = PresetResources.Colors[index, 2], 170 | _isPreset = true, 171 | BackBrush = r => 172 | { 173 | var brush = new LinearGradientBrush(r, 174 | PresetResources.Colors[index, 1], 175 | Color.White, 176 | LinearGradientMode.Horizontal); 177 | brush.SetBlendTriangularShape(0.5f); 178 | return brush; 179 | }, 180 | }; 181 | 182 | bool _disposed; 183 | 184 | [Obsolete("请改用Clear指定是否清理字体和图标")] 185 | [MethodImpl(MethodImplOptions.Synchronized)] 186 | void IDisposable.Dispose() 187 | { 188 | if (_disposed || _isPreset) //不销毁预置对象 189 | return; 190 | 191 | Border.Dispose(); 192 | BackBrush = null; 193 | if (!_keepFont && TextFont != null && !TextFont.IsSystemFont) 194 | { 195 | TextFont.Dispose(); 196 | TextFont = null; 197 | } 198 | 199 | if (!_keepIcon && Icon != null) 200 | { 201 | Icon.Dispose(); 202 | Icon = null; 203 | } 204 | 205 | _disposed = true; 206 | } 207 | } 208 | 209 | /// 210 | /// 预置资源 211 | /// 212 | file static class PresetResources 213 | { 214 | public static readonly Color[,] Colors = 215 | { 216 | //边框色、背景色、阴影色 217 | /*灰*/ { Color.FromArgb(150, 150, 150), Color.FromArgb(245, 245, 245), Color.FromArgb(110, 0, 0, 0) }, 218 | /*绿*/ { Color.FromArgb(0, 189, 0), Color.FromArgb(232, 255, 232), Color.FromArgb(150, 0, 150, 0) }, 219 | /*橙*/ { Color.FromArgb(255, 150, 0), Color.FromArgb(255, 250, 240), Color.FromArgb(150, 250, 100, 0) }, 220 | /*红*/ { Color.FromArgb(255, 79, 79), Color.FromArgb(255, 245, 245), Color.FromArgb(140, 255, 30, 30) } 221 | }; 222 | 223 | //CreateIcon依赖Colors,所以需在Colors后初始化 224 | public static readonly Bitmap[] Icons = 225 | [ 226 | CreateIcon(0), 227 | CreateIcon(1), 228 | CreateIcon(2), 229 | CreateIcon(3) 230 | ]; 231 | 232 | /// 233 | /// 创建图标 234 | /// 235 | /// 0=i;1=√;2=!;3=×;其余=null 236 | static Bitmap CreateIcon(int index) 237 | { 238 | if (index is < 0 or > 3) 239 | return null; 240 | 241 | Graphics g = null; 242 | Pen pen = null; 243 | Brush brush = null; 244 | Bitmap bmp = null; 245 | try 246 | { 247 | bmp = new Bitmap(24, 24); 248 | g = Graphics.FromImage(bmp); 249 | g.SmoothingMode = SmoothingMode.HighQuality; 250 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 251 | 252 | var color = Colors[index, 0]; 253 | if (index == 0) //i 254 | { 255 | brush = new SolidBrush(Color.FromArgb(103, 148, 186)); 256 | g.FillEllipse(brush, 3, 3, 18, 18); 257 | 258 | pen = new Pen(Colors[index, 1], 2); 259 | g.DrawLine(pen, new Point(12, 6), new Point(12, 8)); 260 | g.DrawLine(pen, new Point(12, 10), new Point(12, 18)); 261 | } 262 | else if (index == 1) //√ 263 | { 264 | pen = new Pen(color, 4); 265 | g.DrawLines(pen, new[] { new Point(3, 11), new Point(10, 18), new Point(20, 5) }); 266 | } 267 | else if (index == 2) //! 268 | { 269 | var points = new[] { new Point(12, 3), new Point(3, 20), new Point(21, 20) }; 270 | pen = new Pen(color, 2) { LineJoin = LineJoin.Bevel }; 271 | g.DrawPolygon(pen, points); //描边让尖角变圆润 272 | 273 | brush = new SolidBrush(color); 274 | g.FillPolygon(brush, points); 275 | 276 | pen.Color = Colors[index, 1]; 277 | g.DrawLine(pen, new Point(12, 8), new Point(12, 15)); 278 | g.DrawLine(pen, new Point(12, 17), new Point(12, 19)); 279 | } 280 | else //× 281 | { 282 | pen = new Pen(color, 4); 283 | g.DrawLine(pen, 5, 5, 19, 19); 284 | g.DrawLine(pen, 5, 19, 19, 5); 285 | } 286 | 287 | return bmp; 288 | } 289 | catch 290 | { 291 | bmp?.Dispose(); 292 | throw; 293 | } 294 | finally 295 | { 296 | pen?.Dispose(); 297 | brush?.Dispose(); 298 | g?.Dispose(); 299 | } 300 | } 301 | } 302 | 303 | //干脆自建一个委托,不依赖Func了 304 | /// 305 | /// 画刷选择器委托 306 | /// 307 | public delegate Brush BrushCreator(T arg); -------------------------------------------------------------------------------- /MessageTip/MessageTip/GraphicsUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using System; 4 | using System.Drawing; 5 | using System.Drawing.Drawing2D; 6 | using System.Drawing.Imaging; 7 | using System.Runtime.InteropServices; 8 | 9 | namespace AhDung.Drawing; 10 | 11 | internal static class GraphicsUtils 12 | { 13 | /// 14 | /// 计算指定边界添加描边和阴影后的边界 15 | /// 16 | public static Rectangle GetBounds(Rectangle rectangle, Border border = null, int shadowRadius = 0, int offsetX = 0, int offsetY = 0) 17 | { 18 | if (border != null) 19 | rectangle = border.GetBounds(rectangle); 20 | 21 | var boundsShadow = DropShadow.GetBounds(rectangle, shadowRadius); 22 | boundsShadow.Offset(offsetX, offsetY); 23 | return Rectangle.Union(rectangle, boundsShadow); 24 | } 25 | 26 | /// 27 | /// 测量文本区尺寸 28 | /// 29 | public static SizeF MeasureString(string text, Font font, int width = 0, StringFormat stringFormat = null) => 30 | MeasureString(text, font, new SizeF(width, width > 0 ? 999999 : 0), stringFormat); 31 | 32 | /// 33 | /// 测量文本区尺寸 34 | /// 35 | public static SizeF MeasureString(string text, Font font, SizeF area, StringFormat stringFormat = null) 36 | { 37 | IntPtr dcScreen = IntPtr.Zero; 38 | Graphics g = null; 39 | try 40 | { 41 | dcScreen = GetDC(IntPtr.Zero); 42 | g = Graphics.FromHdc(dcScreen); 43 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 44 | 45 | return g.MeasureString(text, font, area, stringFormat); 46 | } 47 | finally 48 | { 49 | g?.Dispose(); 50 | if (dcScreen != IntPtr.Zero) 51 | { 52 | ReleaseDC(IntPtr.Zero, dcScreen); 53 | } 54 | } 55 | } 56 | 57 | /// 58 | /// 构造圆角矩形 59 | /// 60 | private static GraphicsPath GetRoundedRectangle(Rectangle rectangle, int radius) 61 | { 62 | //合理化圆角半径 63 | radius = Math.Min(radius, Math.Min(rectangle.Width, rectangle.Height) / 2); 64 | 65 | var path = new GraphicsPath(); 66 | if (radius < 1) 67 | { 68 | path.AddRectangle(rectangle); 69 | return path; 70 | } 71 | 72 | int d = radius * 2; 73 | var arc = rectangle with { Width = d, Height = d }; 74 | path.AddArc(arc, 180, 90); 75 | arc.X = rectangle.X + rectangle.Width - d; 76 | path.AddArc(arc, 270, 90); 77 | arc.Y = rectangle.Y + rectangle.Height - d; 78 | path.AddArc(arc, 0, 90); 79 | arc.X = rectangle.X; 80 | path.AddArc(arc, 90, 90); 81 | path.CloseFigure(); 82 | return path; 83 | } 84 | 85 | /// 86 | /// 绘制矩形。可带圆角、阴影 87 | /// 88 | /// 89 | /// 矩形 90 | /// 用于填充的画刷。为null则不填充 91 | /// 边框描述对象。对象无效则不描边 92 | /// 圆角半径 93 | /// 阴影颜色 94 | /// 阴影羽化半径 95 | /// 阴影横向偏移 96 | /// 阴影纵向偏移 97 | public static void DrawRectangle(Graphics g, Rectangle rectangle, Brush brush, Border border, int radius, Color shadowColor, int shadowRadius = 0, int offsetX = 0, int offsetY = 0) 98 | { 99 | if (shadowColor.A == 0 || (shadowRadius == 0 && offsetX == 0 && offsetY == 0)) 100 | { 101 | DrawRectangle(g, rectangle, brush, border, radius); 102 | return; 103 | } 104 | 105 | GraphicsPath path = null; 106 | Bitmap shadow = null; 107 | try 108 | { 109 | path = GetRoundedRectangle(rectangle, radius); 110 | shadow = DropShadow.Create(path, shadowColor, shadowRadius); 111 | 112 | var shadowBounds = DropShadow.GetBounds(rectangle, shadowRadius); 113 | shadowBounds.Offset(offsetX, offsetY); 114 | 115 | g.DrawImageUnscaled(shadow, shadowBounds.Location); 116 | DrawPath(g, path, brush, border); 117 | } 118 | finally 119 | { 120 | path?.Dispose(); 121 | shadow?.Dispose(); 122 | } 123 | } 124 | 125 | /// 126 | /// 画矩形 127 | /// 128 | /// 129 | /// 矩形 130 | /// 用于填充的画刷。为null则不填充 131 | /// 边框描述对象。对象无效则不描边 132 | /// 圆角半径 133 | public static void DrawRectangle(Graphics g, Rectangle rectangle, Brush brush = null, Border border = null, int radius = 0) 134 | { 135 | using var path = GetRoundedRectangle(rectangle, radius); 136 | DrawPath(g, path, brush, border); 137 | } 138 | 139 | /// 140 | /// 画路径 141 | /// 142 | /// 143 | /// 路径 144 | /// 用于填充的画刷。为null则不填充 145 | /// 边框描述对象。对象无效则不描边 146 | public static void DrawPath(Graphics g, GraphicsPath path, Brush brush = null, Border border = null) 147 | { 148 | if (Border.IsValid(border) && border.Behind) 149 | { 150 | g.DrawPath(border.Pen, path); 151 | } 152 | 153 | if (brush != null) 154 | { 155 | g.FillPath(brush, path); 156 | } 157 | 158 | if (Border.IsValid(border) && !border.Behind) 159 | { 160 | g.DrawPath(border.Pen, path); 161 | } 162 | } 163 | 164 | 165 | #region Win32 API 166 | 167 | [DllImport("user32.dll")] 168 | static extern IntPtr GetDC(IntPtr hWnd); 169 | 170 | [DllImport("user32.dll")] 171 | static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); 172 | 173 | #endregion 174 | } 175 | 176 | /// 177 | /// 阴影生成类 178 | /// 179 | /* ================================================================================= 180 | * 算法源于:http://blog.ivank.net/fastest-gaussian-blur.html 181 | * C#版实现取自:http://stackoverflow.com/questions/7364026/algorithm-for-fast-drop-shadow-in-gdi 182 | * 修改+优化:AhDung 183 | * - unsafe转safe 184 | * - RGB色值处理。解决边缘羽化区域黑化问题 185 | * - 减少运算量 186 | * ================================================================================= 187 | */ 188 | file static class DropShadow 189 | { 190 | const int CHANNELS = 4; 191 | const int InflateMultiple = 2; //单边外延radius的倍数 192 | 193 | /// 194 | /// 获取阴影边界。供外部定位阴影用 195 | /// 196 | /// 形状 197 | /// 模糊半径 198 | /// 形状边界 199 | /// 单边外延像素 200 | public static Rectangle GetBounds(GraphicsPath path, int radius, out Rectangle pathBounds, out int inflate) 201 | { 202 | var bounds = pathBounds = Rectangle.Ceiling(path.GetBounds()); 203 | inflate = radius * InflateMultiple; 204 | bounds.Inflate(inflate, inflate); 205 | return bounds; 206 | } 207 | 208 | /// 209 | /// 获取阴影边界 210 | /// 211 | /// 原边界 212 | /// 模糊半径 213 | public static Rectangle GetBounds(Rectangle source, int radius) 214 | { 215 | var inflate = radius * InflateMultiple; 216 | source.Inflate(inflate, inflate); 217 | return source; 218 | } 219 | 220 | /// 221 | /// 创建阴影图片 222 | /// 223 | /// 阴影形状 224 | /// 阴影颜色 225 | /// 模糊半径 226 | public static Bitmap Create(GraphicsPath path, Color color, int radius = 5) 227 | { 228 | var bounds = GetBounds(path, radius, out Rectangle pathBounds, out int inflate); 229 | var shadow = new Bitmap(bounds.Width, bounds.Height); 230 | 231 | if (color.A == 0) 232 | { 233 | return shadow; 234 | } 235 | 236 | //将形状用color色画在阴影区中心 237 | Graphics g = null; 238 | GraphicsPath pathCopy = null; 239 | Matrix matrix = null; 240 | SolidBrush brush = null; 241 | try 242 | { 243 | matrix = new Matrix(); 244 | matrix.Translate(-pathBounds.X + inflate, -pathBounds.Y + inflate); //先清除形状原有偏移再向中心偏移 245 | pathCopy = (GraphicsPath)path.Clone(); //基于形状副本操作 246 | pathCopy.Transform(matrix); 247 | 248 | brush = new SolidBrush(color); 249 | 250 | g = Graphics.FromImage(shadow); 251 | g.SmoothingMode = SmoothingMode.HighQuality; 252 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 253 | g.FillPath(brush, pathCopy); 254 | } 255 | finally 256 | { 257 | g?.Dispose(); 258 | brush?.Dispose(); 259 | pathCopy?.Dispose(); 260 | matrix?.Dispose(); 261 | } 262 | 263 | if (radius <= 0) 264 | { 265 | return shadow; 266 | } 267 | 268 | BitmapData data = null; 269 | try 270 | { 271 | data = shadow.LockBits(new Rectangle(0, 0, shadow.Width, shadow.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 272 | 273 | //两次方框模糊就能达到不错的效果 274 | //var boxes = DetermineBoxes(radius, 3); 275 | BoxBlur(data, radius, color); 276 | BoxBlur(data, radius, color); 277 | //BoxBlur(shadowData, radius); 278 | 279 | return shadow; 280 | } 281 | finally 282 | { 283 | shadow.UnlockBits(data); 284 | } 285 | } 286 | 287 | /// 288 | /// 方框模糊 289 | /// 290 | /// 图像内存数据 291 | /// 模糊半径 292 | /// 透明色值 293 | #if UNSAFE 294 | static unsafe void BoxBlur(BitmapData data, int radius, Color color) 295 | #else 296 | static void BoxBlur(BitmapData data, int radius, Color color) 297 | #endif 298 | { 299 | #if UNSAFE 300 | IntPtr p1 = data1.Scan0; 301 | #else 302 | byte[] p1 = new byte[data.Stride * data.Height]; 303 | Marshal.Copy(data.Scan0, p1, 0, p1.Length); 304 | #endif 305 | //色值处理 306 | //这步的意义在于让图片中的透明像素拥有color的色值(但仍然保持透明) 307 | //这样在混合时才能合出基于color的颜色(只是透明度不同), 308 | //否则它是与RGB(0,0,0)合,就会得到乌黑的渣特技 309 | byte R = color.R, G = color.G, B = color.B; 310 | for (int i = 3; i < p1.Length; i += 4) 311 | { 312 | if (p1[i] == 0) 313 | { 314 | p1[i - 1] = R; 315 | p1[i - 2] = G; 316 | p1[i - 3] = B; 317 | } 318 | } 319 | 320 | var p2 = new byte[p1.Length]; 321 | int radius2 = 2 * radius + 1; 322 | int First, Last, Sum; 323 | int stride = data.Stride, 324 | width = data.Width, 325 | height = data.Height; 326 | 327 | //只处理Alpha通道 328 | 329 | //横向 330 | for (int r = 0; r < height; r++) 331 | { 332 | int start = r * stride; 333 | int left = start; 334 | int right = start + radius * CHANNELS; 335 | 336 | First = p1[start + 3]; 337 | Last = p1[start + stride - 1]; 338 | Sum = (radius + 1) * First; 339 | 340 | for (int column = 0; column < radius; column++) 341 | { 342 | Sum += p1[start + column * CHANNELS + 3]; 343 | } 344 | 345 | for (var column = 0; column <= radius; column++, right += CHANNELS, start += CHANNELS) 346 | { 347 | Sum += p1[right + 3] - First; 348 | p2[start + 3] = (byte)(Sum / radius2); 349 | } 350 | 351 | for (var column = radius + 1; column < width - radius; column++, left += CHANNELS, right += CHANNELS, start += CHANNELS) 352 | { 353 | Sum += p1[right + 3] - p1[left + 3]; 354 | p2[start + 3] = (byte)(Sum / radius2); 355 | } 356 | 357 | for (var column = width - radius; column < width; column++, left += CHANNELS, start += CHANNELS) 358 | { 359 | Sum += Last - p1[left + 3]; 360 | p2[start + 3] = (byte)(Sum / radius2); 361 | } 362 | } 363 | 364 | //纵向 365 | for (int column = 0; column < width; column++) 366 | { 367 | int start = column * CHANNELS; 368 | int top = start; 369 | int bottom = start + radius * stride; 370 | 371 | First = p2[start + 3]; 372 | Last = p2[start + (height - 1) * stride + 3]; 373 | Sum = (radius + 1) * First; 374 | 375 | for (int row = 0; row < radius; row++) 376 | { 377 | Sum += p2[start + row * stride + 3]; 378 | } 379 | 380 | for (int row = 0; row <= radius; row++, bottom += stride, start += stride) 381 | { 382 | Sum += p2[bottom + 3] - First; 383 | p1[start + 3] = (byte)(Sum / radius2); 384 | } 385 | 386 | for (int row = radius + 1; row < height - radius; row++, top += stride, bottom += stride, start += stride) 387 | { 388 | Sum += p2[bottom + 3] - p2[top + 3]; 389 | p1[start + 3] = (byte)(Sum / radius2); 390 | } 391 | 392 | for (int row = height - radius; row < height; row++, top += stride, start += stride) 393 | { 394 | Sum += Last - p2[top + 3]; 395 | p1[start + 3] = (byte)(Sum / radius2); 396 | } 397 | } 398 | #if !UNSAFE 399 | Marshal.Copy(p1, 0, data.Scan0, p1.Length); 400 | #endif 401 | } 402 | 403 | //private static int[] DetermineBoxes(double Sigma, int BoxCount) 404 | //{ 405 | // double IdealWidth = Math.Sqrt((12 * Sigma * Sigma / BoxCount) + 1); 406 | // int Lower = (int)Math.Floor(IdealWidth); 407 | // if (Lower % 2 == 0) 408 | // Lower--; 409 | // int Upper = Lower + 2; 410 | 411 | // double MedianWidth = (12 * Sigma * Sigma - BoxCount * Lower * Lower - 4 * BoxCount * Lower - 3 * BoxCount) / (-4 * Lower - 4); 412 | // int Median = (int)Math.Round(MedianWidth); 413 | 414 | // int[] BoxSizes = new int[BoxCount]; 415 | // for (int i = 0; i < BoxCount; i++) 416 | // BoxSizes[i] = (i < Median) ? Lower : Upper; 417 | // return BoxSizes; 418 | //} 419 | } -------------------------------------------------------------------------------- /Tester/FmTester.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace AhDung 2 | { 3 | sealed partial class FmTester 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.txbMultiline = new System.Windows.Forms.TextBox(); 32 | this.label2 = new System.Windows.Forms.Label(); 33 | this.label3 = new System.Windows.Forms.Label(); 34 | this.nudDelay = new System.Windows.Forms.NumericUpDown(); 35 | this.btnOk = new System.Windows.Forms.Button(); 36 | this.btnWarning = new System.Windows.Forms.Button(); 37 | this.btnError = new System.Windows.Forms.Button(); 38 | this.btnShow = new System.Windows.Forms.Button(); 39 | this.label4 = new System.Windows.Forms.Label(); 40 | this.toolStrip1 = new System.Windows.Forms.ToolStrip(); 41 | this.btnEnter = new System.Windows.Forms.ToolStripButton(); 42 | this.panel1 = new System.Windows.Forms.Panel(); 43 | this.btnShowInPanel = new System.Windows.Forms.Button(); 44 | this.propertyGrid1 = new System.Windows.Forms.PropertyGrid(); 45 | this.splitContainer1 = new System.Windows.Forms.SplitContainer(); 46 | this.txbTestCaret = new System.Windows.Forms.TextBox(); 47 | this.label1 = new System.Windows.Forms.Label(); 48 | this.label6 = new System.Windows.Forms.Label(); 49 | this.label5 = new System.Windows.Forms.Label(); 50 | this.nudFade = new System.Windows.Forms.NumericUpDown(); 51 | this.btnRestore = new System.Windows.Forms.Button(); 52 | ((System.ComponentModel.ISupportInitialize)(this.nudDelay)).BeginInit(); 53 | this.toolStrip1.SuspendLayout(); 54 | this.splitContainer1.Panel1.SuspendLayout(); 55 | this.splitContainer1.Panel2.SuspendLayout(); 56 | this.splitContainer1.SuspendLayout(); 57 | ((System.ComponentModel.ISupportInitialize)(this.nudFade)).BeginInit(); 58 | this.SuspendLayout(); 59 | // 60 | // txbMultiline 61 | // 62 | this.txbMultiline.AcceptsReturn = true; 63 | this.txbMultiline.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 64 | | System.Windows.Forms.AnchorStyles.Right))); 65 | this.txbMultiline.Location = new System.Drawing.Point(14, 155); 66 | this.txbMultiline.Multiline = true; 67 | this.txbMultiline.Name = "txbMultiline"; 68 | this.txbMultiline.Size = new System.Drawing.Size(467, 63); 69 | this.txbMultiline.TabIndex = 4; 70 | this.txbMultiline.Text = "消息可以是多行\r\nThe message can be multiline"; 71 | // 72 | // label2 73 | // 74 | this.label2.AutoSize = true; 75 | this.label2.Location = new System.Drawing.Point(12, 140); 76 | this.label2.Name = "label2"; 77 | this.label2.Size = new System.Drawing.Size(35, 12); 78 | this.label2.TabIndex = 3; 79 | this.label2.Text = "Text:"; 80 | // 81 | // label3 82 | // 83 | this.label3.AutoSize = true; 84 | this.label3.Location = new System.Drawing.Point(12, 17); 85 | this.label3.Name = "label3"; 86 | this.label3.Size = new System.Drawing.Size(35, 12); 87 | this.label3.TabIndex = 3; 88 | this.label3.Text = "Delay"; 89 | // 90 | // nudDelay 91 | // 92 | this.nudDelay.Location = new System.Drawing.Point(53, 15); 93 | this.nudDelay.Maximum = new decimal(new int[] { 94 | 999999, 95 | 0, 96 | 0, 97 | 0}); 98 | this.nudDelay.Minimum = new decimal(new int[] { 99 | 100, 100 | 0, 101 | 0, 102 | -2147483648}); 103 | this.nudDelay.Name = "nudDelay"; 104 | this.nudDelay.Size = new System.Drawing.Size(93, 21); 105 | this.nudDelay.TabIndex = 0; 106 | this.nudDelay.ValueChanged += new System.EventHandler(this.nudDelay_ValueChanged); 107 | // 108 | // btnOk 109 | // 110 | this.btnOk.Location = new System.Drawing.Point(14, 235); 111 | this.btnOk.Name = "btnOk"; 112 | this.btnOk.Size = new System.Drawing.Size(101, 36); 113 | this.btnOk.TabIndex = 5; 114 | this.btnOk.Text = "ShowOk"; 115 | this.btnOk.UseVisualStyleBackColor = true; 116 | this.btnOk.Click += new System.EventHandler(this.btnOk_Click); 117 | // 118 | // btnWarning 119 | // 120 | this.btnWarning.Location = new System.Drawing.Point(121, 235); 121 | this.btnWarning.Name = "btnWarning"; 122 | this.btnWarning.Size = new System.Drawing.Size(101, 36); 123 | this.btnWarning.TabIndex = 6; 124 | this.btnWarning.Text = "ShowWarning"; 125 | this.btnWarning.UseVisualStyleBackColor = true; 126 | this.btnWarning.Click += new System.EventHandler(this.btnWarning_Click); 127 | // 128 | // btnError 129 | // 130 | this.btnError.Location = new System.Drawing.Point(228, 235); 131 | this.btnError.Name = "btnError"; 132 | this.btnError.Size = new System.Drawing.Size(101, 36); 133 | this.btnError.TabIndex = 7; 134 | this.btnError.Text = "ShowError"; 135 | this.btnError.UseVisualStyleBackColor = true; 136 | this.btnError.Click += new System.EventHandler(this.btnError_Click); 137 | // 138 | // btnShow 139 | // 140 | this.btnShow.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 141 | this.btnShow.Location = new System.Drawing.Point(331, 235); 142 | this.btnShow.Name = "btnShow"; 143 | this.btnShow.Size = new System.Drawing.Size(150, 36); 144 | this.btnShow.TabIndex = 8; 145 | this.btnShow.Text = "Show CustomStyle ->"; 146 | this.btnShow.UseVisualStyleBackColor = true; 147 | this.btnShow.Click += new System.EventHandler(this.btnShow_Click); 148 | // 149 | // label4 150 | // 151 | this.label4.AutoSize = true; 152 | this.label4.Location = new System.Drawing.Point(152, 17); 153 | this.label4.Name = "label4"; 154 | this.label4.Size = new System.Drawing.Size(17, 12); 155 | this.label4.TabIndex = 3; 156 | this.label4.Text = "ms"; 157 | // 158 | // toolStrip1 159 | // 160 | this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { 161 | this.btnEnter}); 162 | this.toolStrip1.Location = new System.Drawing.Point(0, 0); 163 | this.toolStrip1.Name = "toolStrip1"; 164 | this.toolStrip1.Size = new System.Drawing.Size(787, 25); 165 | this.toolStrip1.TabIndex = 0; 166 | this.toolStrip1.Text = "toolStrip1"; 167 | // 168 | // btnEnter 169 | // 170 | this.btnEnter.Image = global::AhDung.Properties.Resources.PicDemo; 171 | this.btnEnter.ImageTransparentColor = System.Drawing.Color.Magenta; 172 | this.btnEnter.Name = "btnEnter"; 173 | this.btnEnter.Size = new System.Drawing.Size(263, 22); 174 | this.btnEnter.Text = "Show by ToolStripItem with Default Style"; 175 | this.btnEnter.Click += new System.EventHandler(this.btnEnter_Click); 176 | // 177 | // panel1 178 | // 179 | this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 180 | | System.Windows.Forms.AnchorStyles.Left) 181 | | System.Windows.Forms.AnchorStyles.Right))); 182 | this.panel1.BackColor = System.Drawing.SystemColors.ControlLight; 183 | this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; 184 | this.panel1.Location = new System.Drawing.Point(14, 289); 185 | this.panel1.Name = "panel1"; 186 | this.panel1.Size = new System.Drawing.Size(335, 122); 187 | this.panel1.TabIndex = 10; 188 | // 189 | // btnShowInPanel 190 | // 191 | this.btnShowInPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 192 | this.btnShowInPanel.Location = new System.Drawing.Point(355, 289); 193 | this.btnShowInPanel.Name = "btnShowInPanel"; 194 | this.btnShowInPanel.Size = new System.Drawing.Size(126, 36); 195 | this.btnShowInPanel.TabIndex = 9; 196 | this.btnShowInPanel.Text = " <- Show In Panel"; 197 | this.btnShowInPanel.UseVisualStyleBackColor = true; 198 | this.btnShowInPanel.Click += new System.EventHandler(this.btnShowInPanel_Click); 199 | // 200 | // propertyGrid1 201 | // 202 | this.propertyGrid1.CategoryForeColor = System.Drawing.SystemColors.InactiveCaptionText; 203 | this.propertyGrid1.Dock = System.Windows.Forms.DockStyle.Fill; 204 | this.propertyGrid1.Location = new System.Drawing.Point(0, 29); 205 | this.propertyGrid1.Name = "propertyGrid1"; 206 | this.propertyGrid1.Size = new System.Drawing.Size(279, 394); 207 | this.propertyGrid1.TabIndex = 1; 208 | // 209 | // splitContainer1 210 | // 211 | this.splitContainer1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; 212 | this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; 213 | this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2; 214 | this.splitContainer1.Location = new System.Drawing.Point(0, 25); 215 | this.splitContainer1.Name = "splitContainer1"; 216 | // 217 | // splitContainer1.Panel1 218 | // 219 | this.splitContainer1.Panel1.Controls.Add(this.txbTestCaret); 220 | this.splitContainer1.Panel1.Controls.Add(this.label1); 221 | this.splitContainer1.Panel1.Controls.Add(this.txbMultiline); 222 | this.splitContainer1.Panel1.Controls.Add(this.label6); 223 | this.splitContainer1.Panel1.Controls.Add(this.label4); 224 | this.splitContainer1.Panel1.Controls.Add(this.btnShowInPanel); 225 | this.splitContainer1.Panel1.Controls.Add(this.label2); 226 | this.splitContainer1.Panel1.Controls.Add(this.panel1); 227 | this.splitContainer1.Panel1.Controls.Add(this.label5); 228 | this.splitContainer1.Panel1.Controls.Add(this.label3); 229 | this.splitContainer1.Panel1.Controls.Add(this.btnShow); 230 | this.splitContainer1.Panel1.Controls.Add(this.nudFade); 231 | this.splitContainer1.Panel1.Controls.Add(this.nudDelay); 232 | this.splitContainer1.Panel1.Controls.Add(this.btnError); 233 | this.splitContainer1.Panel1.Controls.Add(this.btnOk); 234 | this.splitContainer1.Panel1.Controls.Add(this.btnWarning); 235 | // 236 | // splitContainer1.Panel2 237 | // 238 | this.splitContainer1.Panel2.Controls.Add(this.propertyGrid1); 239 | this.splitContainer1.Panel2.Controls.Add(this.btnRestore); 240 | this.splitContainer1.Size = new System.Drawing.Size(787, 427); 241 | this.splitContainer1.SplitterDistance = 498; 242 | this.splitContainer1.SplitterWidth = 6; 243 | this.splitContainer1.TabIndex = 1; 244 | // 245 | // txbTestCaret 246 | // 247 | this.txbTestCaret.Location = new System.Drawing.Point(14, 69); 248 | this.txbTestCaret.Multiline = true; 249 | this.txbTestCaret.Name = "txbTestCaret"; 250 | this.txbTestCaret.Size = new System.Drawing.Size(471, 52); 251 | this.txbTestCaret.TabIndex = 3; 252 | this.txbTestCaret.Text = "Click in this textbox and try press Enter key\r\n点进来回车试试\r\n的发萨达佛萨发的萨法阿斯顿发送到发送到发是防守打法" + 253 | "说"; 254 | // 255 | // label1 256 | // 257 | this.label1.AutoSize = true; 258 | this.label1.Location = new System.Drawing.Point(12, 52); 259 | this.label1.Name = "label1"; 260 | this.label1.Size = new System.Drawing.Size(143, 12); 261 | this.label1.TabIndex = 12; 262 | this.label1.Text = "for test show by Caret:"; 263 | // 264 | // label6 265 | // 266 | this.label6.Anchor = System.Windows.Forms.AnchorStyles.Top; 267 | this.label6.AutoSize = true; 268 | this.label6.Location = new System.Drawing.Point(346, 17); 269 | this.label6.Name = "label6"; 270 | this.label6.Size = new System.Drawing.Size(17, 12); 271 | this.label6.TabIndex = 3; 272 | this.label6.Text = "ms"; 273 | // 274 | // label5 275 | // 276 | this.label5.Anchor = System.Windows.Forms.AnchorStyles.Top; 277 | this.label5.AutoSize = true; 278 | this.label5.Location = new System.Drawing.Point(212, 17); 279 | this.label5.Name = "label5"; 280 | this.label5.Size = new System.Drawing.Size(29, 12); 281 | this.label5.TabIndex = 3; 282 | this.label5.Text = "Fade"; 283 | // 284 | // nudFade 285 | // 286 | this.nudFade.Anchor = System.Windows.Forms.AnchorStyles.Top; 287 | this.nudFade.Location = new System.Drawing.Point(247, 15); 288 | this.nudFade.Maximum = new decimal(new int[] { 289 | 999999, 290 | 0, 291 | 0, 292 | 0}); 293 | this.nudFade.Minimum = new decimal(new int[] { 294 | 100, 295 | 0, 296 | 0, 297 | -2147483648}); 298 | this.nudFade.Name = "nudFade"; 299 | this.nudFade.Size = new System.Drawing.Size(93, 21); 300 | this.nudFade.TabIndex = 1; 301 | this.nudFade.ValueChanged += new System.EventHandler(this.nudFade_ValueChanged); 302 | // 303 | // btnRestore 304 | // 305 | this.btnRestore.Dock = System.Windows.Forms.DockStyle.Top; 306 | this.btnRestore.Location = new System.Drawing.Point(0, 0); 307 | this.btnRestore.Name = "btnRestore"; 308 | this.btnRestore.Size = new System.Drawing.Size(279, 29); 309 | this.btnRestore.TabIndex = 0; 310 | this.btnRestore.Text = "Custom TipStyle (Click to Restore)"; 311 | this.btnRestore.UseVisualStyleBackColor = true; 312 | this.btnRestore.Click += new System.EventHandler(this.btnRestore_Click); 313 | // 314 | // FmTester 315 | // 316 | this.AcceptButton = this.btnOk; 317 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); 318 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 319 | this.ClientSize = new System.Drawing.Size(787, 452); 320 | this.Controls.Add(this.splitContainer1); 321 | this.Controls.Add(this.toolStrip1); 322 | this.MinimumSize = new System.Drawing.Size(461, 230); 323 | this.Name = "FmTester"; 324 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 325 | this.Text = "Tester"; 326 | ((System.ComponentModel.ISupportInitialize)(this.nudDelay)).EndInit(); 327 | this.toolStrip1.ResumeLayout(false); 328 | this.toolStrip1.PerformLayout(); 329 | this.splitContainer1.Panel1.ResumeLayout(false); 330 | this.splitContainer1.Panel1.PerformLayout(); 331 | this.splitContainer1.Panel2.ResumeLayout(false); 332 | this.splitContainer1.ResumeLayout(false); 333 | ((System.ComponentModel.ISupportInitialize)(this.nudFade)).EndInit(); 334 | this.ResumeLayout(false); 335 | this.PerformLayout(); 336 | 337 | } 338 | 339 | #endregion 340 | 341 | private System.Windows.Forms.TextBox txbMultiline; 342 | private System.Windows.Forms.Label label2; 343 | private System.Windows.Forms.Label label3; 344 | private System.Windows.Forms.NumericUpDown nudDelay; 345 | private System.Windows.Forms.Button btnOk; 346 | private System.Windows.Forms.Button btnWarning; 347 | private System.Windows.Forms.Button btnError; 348 | private System.Windows.Forms.Button btnShow; 349 | private System.Windows.Forms.Label label4; 350 | private System.Windows.Forms.ToolStrip toolStrip1; 351 | private System.Windows.Forms.ToolStripButton btnEnter; 352 | private System.Windows.Forms.Panel panel1; 353 | private System.Windows.Forms.Button btnShowInPanel; 354 | private System.Windows.Forms.PropertyGrid propertyGrid1; 355 | private System.Windows.Forms.SplitContainer splitContainer1; 356 | private System.Windows.Forms.Label label6; 357 | private System.Windows.Forms.Label label5; 358 | private System.Windows.Forms.NumericUpDown nudFade; 359 | private System.Windows.Forms.Button btnRestore; 360 | private System.Windows.Forms.TextBox txbTestCaret; 361 | private System.Windows.Forms.Label label1; 362 | } 363 | } -------------------------------------------------------------------------------- /MessageTip/MessageTip/MessageTip.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) AhDung. All Rights Reserved. 2 | 3 | using AhDung.Drawing; 4 | using System; 5 | using System.Collections; 6 | using System.ComponentModel; 7 | using System.Diagnostics; 8 | using System.Drawing; 9 | using System.Drawing.Drawing2D; 10 | using System.Drawing.Text; 11 | using System.Runtime.CompilerServices; 12 | using System.Runtime.InteropServices; 13 | using System.Threading; 14 | using System.Windows.Forms; 15 | 16 | namespace AhDung; 17 | 18 | //求更高效的阴影画法 19 | 20 | /// 21 | /// 轻便消息窗 22 | /// 23 | public static class MessageTip 24 | { 25 | //默认字体。当样式中的Font==null时用该字体替换 26 | static readonly Font DefaultFont = new(SystemFonts.MessageBoxFont.FontFamily, 12); 27 | 28 | //文本格式。用于测量和绘制 29 | static readonly StringFormat DefaultStringFormat = new(StringFormatFlags.FitBlackBox | StringFormatFlags.LineLimit | StringFormatFlags.NoClip) 30 | { 31 | Alignment = StringAlignment.Near, 32 | HotkeyPrefix = HotkeyPrefix.None, 33 | LineAlignment = StringAlignment.Near, 34 | Trimming = StringTrimming.None, 35 | }; 36 | 37 | /// 38 | /// 获取或设置默认消息样式 39 | /// 40 | public static TipStyle DefaultStyle { get; set; } = TipStyle.Gray; 41 | 42 | /// 43 | /// 获取或设置良好消息样式 44 | /// 45 | public static TipStyle OkStyle { get; set; } = TipStyle.Green; 46 | 47 | /// 48 | /// 获取或设置警告消息样式 49 | /// 50 | public static TipStyle WarningStyle { get; set; } = TipStyle.Orange; 51 | 52 | /// 53 | /// 获取或设置出错消息样式 54 | /// 55 | public static TipStyle ErrorStyle { get; set; } = TipStyle.Red; 56 | 57 | /// 58 | /// 获取或设置全局淡入淡出时长(毫秒)。默认100。呈现总时长=淡入+停留+淡出,即Fade x 2 + Delay 59 | /// 60 | public static int Fade { get; set; } = 100; 61 | 62 | /// 63 | /// 获取或设置全局消息停留时长(毫秒)。默认1000。呈现总时长=淡入+停留+淡出,即Fade x 2 + Delay 64 | /// 65 | public static int Delay { get; set; } = 1000; 66 | 67 | /// 68 | /// 在指定控件附近显示良好消息 69 | /// 70 | /// 控件或工具栏项 71 | /// 消息文本 72 | /// 消息停留时长(ms)。为负时使用全局时长 73 | /// 是否浮动 74 | /// 是否在控件中央显示,不指定则自动判断 75 | public static void ShowOk(Component controlOrItem, string text = null, int delay = -1, bool floating = true, bool? centerInControl = null) => 76 | Show(controlOrItem, text, OkStyle ?? TipStyle.Green, delay, floating, centerInControl); 77 | 78 | /// 79 | /// 显示良好消息 80 | /// 81 | /// 消息文本 82 | /// 消息停留时长(ms)。为负时使用全局时长 83 | /// 是否浮动 84 | /// 消息窗显示位置。不指定则智能判定,当由工具栏项(ToolStripItem)弹出时,请指定该参数或使用接收控件的重载 85 | /// 是否以point参数为中心进行呈现。为false则是在其附近呈现 86 | public static void ShowOk(string text = null, int delay = -1, bool floating = true, Point? point = null, bool centerByPoint = false) => 87 | Show(text, OkStyle ?? TipStyle.Green, delay, floating, point, centerByPoint); 88 | 89 | /// 90 | /// 在指定控件附近显示警告消息 91 | /// 92 | /// 控件或工具栏项 93 | /// 消息文本 94 | /// 消息停留时长(ms)。默认1秒,若要使用全局时长请设为-1 95 | /// 是否浮动 96 | /// 是否在控件中央显示,不指定则自动判断 97 | public static void ShowWarning(Component controlOrItem, string text = null, int delay = -1, bool floating = false, bool? centerInControl = null) => 98 | Show(controlOrItem, text, WarningStyle ?? TipStyle.Orange, delay, floating, centerInControl); 99 | 100 | /// 101 | /// 显示警告消息 102 | /// 103 | /// 消息文本 104 | /// 消息停留时长(ms)。默认1秒,若要使用全局时长请设为-1 105 | /// 是否浮动 106 | /// 消息窗显示位置。不指定则智能判定,当由工具栏项(ToolStripItem)弹出时,请指定该参数或使用接收控件的重载 107 | /// 是否以point参数为中心进行呈现。为false则是在其附近呈现 108 | public static void ShowWarning(string text = null, int delay = -1, bool floating = false, Point? point = null, bool centerByPoint = false) => 109 | Show(text, WarningStyle ?? TipStyle.Orange, delay, floating, point, centerByPoint); 110 | 111 | /// 112 | /// 在指定控件附近显示出错消息 113 | /// 114 | /// 控件或工具栏项 115 | /// 消息文本 116 | /// 消息停留时长(ms)。默认1秒,若要使用全局时长请设为-1 117 | /// 是否浮动 118 | /// 是否在控件中央显示,不指定则自动判断 119 | public static void ShowError(Component controlOrItem, string text = null, int delay = -1, bool floating = false, bool? centerInControl = null) => 120 | Show(controlOrItem, text, ErrorStyle ?? TipStyle.Red, delay, floating, centerInControl); 121 | 122 | /// 123 | /// 显示出错消息 124 | /// 125 | /// 消息文本 126 | /// 消息停留时长(ms)。默认1秒,若要使用全局时长请设为-1 127 | /// 是否浮动 128 | /// 消息窗显示位置。不指定则智能判定,当由工具栏项(ToolStripItem)弹出时,请指定该参数或使用接收控件的重载 129 | /// 是否以point参数为中心进行呈现。为false则是在其附近呈现 130 | public static void ShowError(string text = null, int delay = -1, bool floating = false, Point? point = null, bool centerByPoint = false) => 131 | Show(text, ErrorStyle ?? TipStyle.Red, delay, floating, point, centerByPoint); 132 | 133 | /// 134 | /// 在指定控件附近显示消息 135 | /// 136 | /// 控件或工具栏项 137 | /// 消息文本 138 | /// 消息样式。不指定则使用默认样式 139 | /// 消息停留时长(ms)。为负时使用全局时长 140 | /// 是否浮动 141 | /// 是否在控件中央显示,不指定则自动判断 142 | public static void Show(Component controlOrItem, string text, TipStyle style = null, int delay = -1, bool floating = false, bool? centerInControl = null) 143 | { 144 | _ = controlOrItem ?? throw new ArgumentNullException(nameof(controlOrItem)); 145 | Show(text, style, delay, floating, GetCenterPosition(controlOrItem), centerInControl ?? IsContainerLike(controlOrItem)); 146 | } 147 | 148 | /* 149 | * 目前的实现是show的时候新建消息窗并加入一个集合, 150 | * 当集合有元素时会启动一个线程集中跑动画,线程按照指定帧率循环更新集合中的所有消息窗的状态(位置和透明度)。 151 | * 动画跑完的消息窗会销毁和移除集合,集合为空时线程跑完结束。 152 | * 也就是所有消息的所有动画在一个线程搞掂,相比之前每个消息窗占1个(位移还要再占1个)线程,资源占用大幅优化。 153 | */ 154 | 155 | static readonly ArrayList _layers = new(5); 156 | const int MSPF = 15; //每帧毫秒数 157 | 158 | /// 159 | /// 显示消息 160 | /// 161 | /// 消息文本 162 | /// 消息样式。不指定则使用默认样式 163 | /// 消息停留时长(ms)。为负时使用全局时长 164 | /// 是否浮动 165 | /// 消息窗显示位置。不指定则智能判定,当由工具栏项(ToolStripItem)弹出时,请指定该参数或使用接收控件的重载 166 | /// 是否以point参数为中心进行呈现。为false则是在其附近呈现 167 | public static void Show(string text, TipStyle style = null, int delay = -1, bool floating = false, Point? point = null, bool centerByPoint = false) 168 | { 169 | var fadeFrames = Fade / MSPF; 170 | var totalFrames = fadeFrames * 2 + (delay < 0 ? Delay : delay) / MSPF; 171 | if (totalFrames <= 0) 172 | throw new ArgumentOutOfRangeException("总帧数小于等于0!请检查Fade和delay。", (Exception)null); 173 | 174 | var basePoint = point ?? DetermineHotPoint(); 175 | 176 | var layer = new LayeredWindow 177 | { 178 | BackgroundImage = CreateTipImage(text, style ?? DefaultStyle ?? TipStyle.Gray, out var contentBounds), 179 | Alpha = 0, 180 | Location = GetLocation(contentBounds, basePoint, centerByPoint, out var floatDown), 181 | MouseThrough = true, 182 | TopMost = true, 183 | Tag = new ShowData 184 | { 185 | FadeFrames = fadeFrames, 186 | TotalFrames = totalFrames, 187 | FloatOffset = floating ? floatDown ? 1 : -1 : 0, 188 | }, 189 | }; 190 | 191 | lock (_layers.SyncRoot) 192 | { 193 | _layers.Add(layer); 194 | if (_layers.Count == 1) 195 | StartAnimation(); 196 | } 197 | } 198 | 199 | static void StartAnimation() => new Thread(_ => //用线程池会偶尔造成消息窗冻结,win10下出现 200 | { 201 | var stopwatch = new Stopwatch(); 202 | SwitchTimerResolution(true); 203 | 204 | try 205 | { 206 | //一圈就是一帧。经测timer精度不如循环 207 | while (true) 208 | { 209 | #if NET40_OR_GREATER || NET 210 | stopwatch.Restart(); 211 | #else 212 | stopwatch.Reset(); 213 | stopwatch.Start(); 214 | #endif 215 | 216 | //更新每个消息窗的当前帧 217 | lock (_layers.SyncRoot) 218 | { 219 | for (var i = 0; i < _layers.Count; i++) 220 | { 221 | var layer = (LayeredWindow)_layers[i]; 222 | var data = (ShowData)layer!.Tag; 223 | 224 | layer.SuspendLayout(); 225 | 226 | //淡入 227 | if (data.Frame <= data.FadeFrames) 228 | { 229 | if (data.Frame == 0) 230 | layer.Show(); //不能在外面show好,因为要与close处于同一线程 231 | 232 | layer.Opacity = data.FadeFrames == 0 ? 1 : data.Frame / (float)data.FadeFrames; 233 | } 234 | //淡出 235 | else if (data.TotalFrames - data.Frame is var countdown && countdown <= data.FadeFrames) 236 | { 237 | if (countdown <= 0) 238 | { 239 | layer.Close(); 240 | _layers.RemoveAt(i); 241 | i--; 242 | continue; 243 | } 244 | 245 | layer.Opacity = countdown / (float)data.FadeFrames; 246 | } 247 | 248 | //位移。实践中每两帧移动1px较合适 249 | if (data.FloatOffset != 0 && data.Frame % 2 == 0) 250 | layer.Top += data.FloatOffset; 251 | 252 | layer.ResumeLayout(); 253 | data.Frame++; 254 | } 255 | 256 | if (_layers.Count == 0) 257 | { 258 | _layers.Capacity = 5; 259 | break; 260 | } 261 | } 262 | 263 | //耗时补偿 264 | Thread.Sleep(Math.Max(0, MSPF - (int)stopwatch.ElapsedMilliseconds)); 265 | } 266 | } 267 | finally 268 | { 269 | SwitchTimerResolution(false); 270 | stopwatch.Stop(); 271 | } 272 | }) { Name = "T_MessageTip_Animator", IsBackground = true }.Start(); 273 | 274 | 275 | //高精计时器开关。不启用的话Thread.Sleep的稳定性没法看 276 | //参考:http://mirrors.arcadecontrols.com/www.sysinternals.com/Information/HighResolutionTimers.html 277 | static void SwitchTimerResolution(bool enable) 278 | { 279 | _ = enable ? timeBeginPeriod(1) : timeEndPeriod(1); 280 | 281 | [DllImport("Winmm.dll")] 282 | static extern uint timeBeginPeriod(uint uPeriod); 283 | 284 | [DllImport("Winmm.dll")] 285 | static extern uint timeEndPeriod(uint uPeriod); 286 | } 287 | 288 | class ShowData 289 | { 290 | public int Frame { get; set; } 291 | 292 | public int FadeFrames { get; set; } 293 | 294 | public int TotalFrames { get; set; } 295 | 296 | public int FloatOffset { get; set; } 297 | } 298 | 299 | /// 300 | /// 判定活动点 301 | /// 302 | static Point DetermineHotPoint() 303 | { 304 | var point = Control.MousePosition; 305 | 306 | var focusControl = Control.FromHandle(GetFocus()); 307 | if (focusControl is TextBoxBase) //若焦点是文本框,取光标位置 308 | { 309 | GetCaretPos(out var pt); 310 | pt.Y += focusControl.Font.Height / 2; 311 | point = focusControl.PointToScreen(pt); 312 | } 313 | else if (focusControl is ButtonBase) //若焦点是按钮,取按钮中心点 314 | { 315 | point = GetCenterPosition(focusControl); 316 | } 317 | 318 | return point; 319 | 320 | [DllImport("User32.dll", SetLastError = true)] 321 | static extern bool GetCaretPos(out Point pt); 322 | 323 | [DllImport("user32.dll")] 324 | static extern IntPtr GetFocus(); 325 | } 326 | 327 | /// 328 | /// 创建消息窗图像,同时输出内容区,用于外部定位 329 | /// 330 | [MethodImpl(MethodImplOptions.Synchronized)] //都在UI线程Show的话倒不需要 331 | static Bitmap CreateTipImage(string text, TipStyle style, out Rectangle contentBounds) 332 | { 333 | var size = Size.Empty; 334 | var iconBounds = Rectangle.Empty; 335 | var textBounds = Rectangle.Empty; 336 | 337 | if (style.Icon != null) 338 | { 339 | size = style.Icon.Size; 340 | iconBounds.Size = size; 341 | textBounds.X = size.Width; 342 | } 343 | 344 | if (text?.Length is > 0) 345 | { 346 | if (style.Icon != null) 347 | { 348 | size.Width += style.IconSpacing; 349 | textBounds.X += style.IconSpacing; 350 | } 351 | 352 | textBounds.Size = Size.Truncate(GraphicsUtils.MeasureString(text, style.TextFont ?? DefaultFont, 0, DefaultStringFormat)); 353 | size.Width += textBounds.Width; 354 | 355 | if (size.Height < textBounds.Height) 356 | { 357 | size.Height = textBounds.Height; 358 | } 359 | else if (size.Height > textBounds.Height) //若文字没有图标高,令文字与图标垂直居中,否则与图标平齐 360 | { 361 | textBounds.Y += (size.Height - textBounds.Height) / 2; 362 | } 363 | 364 | textBounds.Offset(style.TextOffset); 365 | } 366 | 367 | size += style.Padding.Size; 368 | iconBounds.Offset(style.Padding.Left, style.Padding.Top); 369 | textBounds.Offset(style.Padding.Left, style.Padding.Top); 370 | 371 | contentBounds = new Rectangle(Point.Empty, size); 372 | var fullBounds = GraphicsUtils.GetBounds(contentBounds, style.Border, style.ShadowRadius, style.ShadowOffset.X, style.ShadowOffset.Y); 373 | contentBounds.Offset(-fullBounds.X, -fullBounds.Y); 374 | iconBounds.Offset(-fullBounds.X, -fullBounds.Y); 375 | textBounds.Offset(-fullBounds.X, -fullBounds.Y); 376 | 377 | var bmp = new Bitmap(fullBounds.Width, fullBounds.Height); 378 | 379 | Graphics g = null; 380 | Brush backBrush = null; 381 | Brush textBrush = null; 382 | try 383 | { 384 | g = Graphics.FromImage(bmp); 385 | g.SmoothingMode = SmoothingMode.HighQuality; 386 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 387 | 388 | backBrush = (style.BackBrush ?? (_ => new SolidBrush(style.BackColor)))(contentBounds); 389 | GraphicsUtils.DrawRectangle(g, contentBounds, 390 | backBrush, 391 | style.Border, 392 | style.CornerRadius, 393 | style.ShadowColor, 394 | style.ShadowRadius, 395 | style.ShadowOffset.X, 396 | style.ShadowOffset.Y); 397 | 398 | if (style.Icon != null) 399 | { 400 | //DEBUG: g.DrawRectangle(new Border(Color.Red) { Width = 1, Direction = Direction.Inner }.Pen, iconBounds); 401 | g.DrawImageUnscaled(style.Icon, iconBounds.Location); 402 | } 403 | 404 | if (text?.Length is > 0) 405 | { 406 | textBrush = new SolidBrush(style.TextColor); 407 | //DEBUG: g.DrawRectangle(new Border(Color.Red){ Width=1, Direction= Direction.Inner}.Pen, textBounds); 408 | g.DrawString(text, style.TextFont ?? DefaultFont, textBrush, textBounds.Location, DefaultStringFormat); 409 | } 410 | 411 | g.Flush(); 412 | return bmp; 413 | } 414 | finally 415 | { 416 | g?.Dispose(); 417 | backBrush?.Dispose(); 418 | textBrush?.Dispose(); 419 | } 420 | } 421 | 422 | /// 423 | /// 根据基准点处理窗体显示位置 424 | /// 425 | /// 内容区。依据该区域处理定位,而不是根据整个消息窗图像,因为阴影也许偏移很大 426 | /// 定位参考点 427 | /// 是否以参考点为中心呈现。false则是在参考点附近呈现 428 | /// 指示是否应当向下浮动(当太靠近屏幕顶部时)。默认是向上 429 | static Point GetLocation(Rectangle contentBounds, Point basePoint, bool centerByBasePoint, out bool floatDown) 430 | { 431 | //以基准点所在屏为界 432 | var screen = Screen.FromPoint(basePoint).Bounds; 433 | 434 | var p = basePoint; 435 | p.X -= contentBounds.Width / 2; 436 | 437 | //横向处理。距离屏幕左右两边太近时的处理 438 | //多屏下left可能为负,所以right = width - (-left) = width + left 439 | var spacing = 10; //至少距离边缘多少像素 440 | int left, right; 441 | if (p.X < (left = screen.Left + spacing)) 442 | { 443 | p.X = left; 444 | } 445 | else if (p.X > (right = screen.Width + screen.Left - spacing - contentBounds.Width)) 446 | { 447 | p.X = right; 448 | } 449 | 450 | //纵向处理 451 | if (centerByBasePoint) 452 | { 453 | p.Y -= contentBounds.Height / 2; 454 | } 455 | else 456 | { 457 | spacing = 20; //错开基准点上下20像素 458 | p.Y -= contentBounds.Height + spacing; 459 | } 460 | 461 | floatDown = false; 462 | if (p.Y < screen.Top + 50) //若太靠屏幕顶部 463 | { 464 | if (!centerByBasePoint) 465 | { 466 | p.Y += contentBounds.Height + 2 * spacing; //在下方错开 467 | } 468 | 469 | floatDown = true; //动画改为下降 470 | } 471 | 472 | p.Offset(-contentBounds.X, -contentBounds.Y); 473 | return p; 474 | } 475 | 476 | /// 477 | /// 获取控件中心点的屏幕坐标 478 | /// 479 | static Point GetCenterPosition(Component controlOrItem) 480 | { 481 | if (controlOrItem is Control c) 482 | { 483 | var size = c.ClientSize; 484 | return c.PointToScreen(new Point(size.Width / 2, size.Height / 2)); 485 | } 486 | 487 | if (controlOrItem is ToolStripItem item) 488 | { 489 | var pos = item.Bounds.Location; 490 | pos.X += item.Width / 2; 491 | pos.Y += item.Height / 2; 492 | return item.Owner.PointToScreen(pos); 493 | } 494 | 495 | throw new ArgumentException("参数只能是Control或ToolStripItem!"); 496 | } 497 | 498 | /// 499 | /// 判断控件看起来是否像容器(占一定面积那种) 500 | /// 501 | static bool IsContainerLike(Component controlOrItem) => 502 | controlOrItem is ContainerControl 503 | or GroupBox 504 | or Panel 505 | or TabControl 506 | #if !NET 507 | or DataGrid 508 | #endif 509 | or DataGridView 510 | or ListBox 511 | or ListView 512 | or TextBox { Multiline: true } 513 | or RichTextBox { Multiline: true }; 514 | } -------------------------------------------------------------------------------- /MessageTip/MessageTip/LayeredWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Drawing; 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | using System.Security; 7 | 8 | namespace AhDung; 9 | 10 | /// 11 | /// 简易层窗体 12 | /// 13 | [SuppressUnmanagedCodeSecurity] 14 | public class LayeredWindow : IDisposable 15 | { 16 | const string ClassName = "AhDungLayeredWindow"; 17 | const int DefaultWidth = 200; 18 | const int DefaultHeight = 200; 19 | static WndProcDelegate _wndProc; 20 | 21 | readonly Color _backgroundColor = SystemColors.Control; 22 | int _top; 23 | int _left; 24 | Image _backgroundImage; 25 | 26 | IntPtr _defaultHBmp; 27 | IntPtr _dcMemory; 28 | IntPtr _hBmp; 29 | IntPtr _oldObj; 30 | 31 | IntPtr _activeWindow; //用于模式显示时,记录并disable原窗体,然后在本类关闭后enable它 32 | bool _continueLoop = true; 33 | short _wndClass; 34 | IntPtr _hWnd; 35 | 36 | BLENDFUNCTION _blend = new() 37 | { 38 | BlendOp = 0, //AC_SRC_OVER 39 | BlendFlags = 0, 40 | SourceConstantAlpha = 255, //透明度 41 | AlphaFormat = 1 //AC_SRC_ALPHA 42 | }; 43 | 44 | bool _visible; 45 | bool _layoutSuspended; 46 | 47 | /// 48 | /// 窗体显示时 49 | /// 50 | public event EventHandler Showing; 51 | 52 | /// 53 | /// 窗体关闭时 54 | /// 55 | public event CancelEventHandler Closing; 56 | 57 | static IntPtr _hInstance; 58 | 59 | static IntPtr HInstance 60 | { 61 | get 62 | { 63 | if (_hInstance == IntPtr.Zero) 64 | _hInstance = Marshal.GetHINSTANCE(Assembly.GetEntryAssembly()!.ManifestModule); 65 | 66 | return _hInstance; 67 | } 68 | } 69 | 70 | /// 71 | /// 获取窗体位置。内部用 72 | /// 73 | PointOrSize LocationInternal => new(_left, _top); 74 | 75 | /// 76 | /// 获取窗体尺寸。内部用 77 | /// 78 | PointOrSize SizeInternal => new(Width, Height); 79 | 80 | /// 81 | /// 窗体句柄 82 | /// 83 | public IntPtr Handle => _hWnd; 84 | 85 | /// 86 | /// 获取或设置窗体可见性 87 | /// 88 | public bool Visible 89 | { 90 | get => _visible; 91 | set 92 | { 93 | if (_visible == value) 94 | return; 95 | 96 | _visible = value; //需在Update前设置,不然Update中检测到未显示也不会更新 97 | 98 | if (value) 99 | { 100 | TryUpdate(); 101 | ShowWindow(_hWnd, Activation ? 5 /*SW_SHOW*/ : 8 /*SW_SHOWNA*/); 102 | } 103 | else 104 | ShowWindow(_hWnd, 0 /*SW_HIDE*/); 105 | } 106 | } 107 | 108 | /// 109 | /// 获取或设置左边缘坐标 110 | /// 111 | public int Left 112 | { 113 | get => _left; 114 | set 115 | { 116 | if (_left != value) 117 | { 118 | _left = value; 119 | TryUpdate(); 120 | } 121 | } 122 | } 123 | 124 | /// 125 | /// 获取或设置上边缘坐标 126 | /// 127 | public int Top 128 | { 129 | get => _top; 130 | set 131 | { 132 | if (_top != value) 133 | { 134 | _top = value; 135 | TryUpdate(); 136 | } 137 | } 138 | } 139 | 140 | /// 141 | /// 获取或设置定位 142 | /// 143 | public Point Location 144 | { 145 | get => new(_left, _top); 146 | set 147 | { 148 | if (_left != value.X || _top != value.Y) 149 | { 150 | _left = value.X; 151 | _top = value.Y; 152 | TryUpdate(); 153 | } 154 | } 155 | } 156 | 157 | /// 158 | /// 获取窗体尺寸。尺寸与匹配,只能通过修改背景图修改 159 | /// 160 | public Size Size => new(Width, Height); 161 | 162 | /// 163 | /// 获取窗体矩形 164 | /// 165 | public Rectangle ClientRectangle => new(Point.Empty, Size); 166 | 167 | /// 168 | /// 获取窗体在桌面上的边界 169 | /// 170 | public Rectangle DesktopBounds => new(Location, Size); 171 | 172 | /// 173 | /// 获取窗体宽度 174 | /// 175 | public int Width { get; private set; } = DefaultWidth; 176 | 177 | /// 178 | /// 获取窗体高度 179 | /// 180 | public int Height { get; private set; } = DefaultHeight; 181 | 182 | /// 183 | /// 获取或设置窗体透明度。0=完全透明;1=不透明 184 | /// 185 | /// 的包装,建议优先用Alpha。 186 | /// 187 | public float Opacity 188 | { 189 | get => Alpha / 255f; 190 | set 191 | { 192 | if (value is < 0 or > 1) 193 | throw new ArgumentOutOfRangeException(); 194 | 195 | Alpha = (byte)(value * 255); 196 | } 197 | } 198 | 199 | /// 200 | /// 获取或设置窗体透明度。0=完全透明;255=不透明 201 | /// 202 | public byte Alpha 203 | { 204 | get => _blend.SourceConstantAlpha; 205 | set 206 | { 207 | if (_blend.SourceConstantAlpha != value) 208 | { 209 | _blend.SourceConstantAlpha = value; 210 | TryUpdate(); 211 | } 212 | } 213 | } 214 | 215 | /// 216 | /// 获取或设置名称 217 | /// 218 | public string Name { get; set; } 219 | 220 | /// 221 | /// 指示窗体是否以模式状态打开 222 | /// 223 | public bool IsModal { get; private set; } 224 | 225 | /// 226 | /// 是否置顶 227 | /// 228 | public bool TopMost { get; set; } 229 | 230 | /// 231 | /// 是否在显示后激活本窗体。模式显示时强制为true 232 | /// 233 | public bool Activation { get; set; } 234 | 235 | /// 236 | /// 是否在任务栏显示 237 | /// 238 | public bool ShowInTaskbar { get; set; } 239 | 240 | /// 241 | /// 是否让鼠标事件穿透本窗体 242 | /// 243 | public bool MouseThrough { get; set; } 244 | 245 | /// 246 | /// 指示窗体是否已释放 247 | /// 248 | public bool IsDisposed { get; private set; } 249 | 250 | /// 251 | /// 获取或设置自定对象 252 | /// 253 | public object Tag { get; set; } 254 | 255 | /// 256 | /// 获取或设置背景图片。设置图片后窗体尺寸将调整为图片大小 257 | /// 258 | public Image BackgroundImage 259 | { 260 | get => _backgroundImage; 261 | set 262 | { 263 | if (_backgroundImage == value) 264 | return; 265 | 266 | if (value is not null and not Bitmap) 267 | throw new ArgumentException("目前只接受位图!"); 268 | 269 | //清理上个图的资源 270 | if (_oldObj != IntPtr.Zero) 271 | SelectObject(_dcMemory, _oldObj); 272 | 273 | if (_hBmp != IntPtr.Zero) 274 | { 275 | DeleteObject(_hBmp); 276 | _hBmp = IntPtr.Zero; 277 | } 278 | 279 | if (value == null) 280 | { 281 | Width = DefaultWidth; 282 | Height = DefaultHeight; 283 | _oldObj = SelectObject(_dcMemory, _defaultHBmp); 284 | } 285 | else 286 | { 287 | Width = value.Width; 288 | Height = value.Height; 289 | _hBmp = ((Bitmap)value).GetHbitmap(Color.Empty); 290 | _oldObj = SelectObject(_dcMemory, _hBmp); 291 | } 292 | 293 | _backgroundImage = value; 294 | TryUpdate(); 295 | } 296 | } 297 | 298 | /// 299 | /// 创建层窗体 300 | /// 301 | public LayeredWindow() 302 | { 303 | RegisterWindowClass(); 304 | _dcMemory = CreateCompatibleDC(IntPtr.Zero); 305 | 306 | using var bmp = new Bitmap(DefaultWidth, DefaultHeight); 307 | using var g = Graphics.FromImage(bmp); 308 | 309 | g.Clear(_backgroundColor); 310 | g.Flush(); 311 | _defaultHBmp = bmp.GetHbitmap(); 312 | _oldObj = SelectObject(_dcMemory, _defaultHBmp); 313 | } 314 | 315 | /// 316 | /// 注册私有窗口类 317 | /// 318 | void RegisterWindowClass() 319 | { 320 | _wndProc ??= WndProc; 321 | 322 | var wc = new WNDCLASS 323 | { 324 | hInstance = HInstance, 325 | lpszClassName = ClassName, 326 | lpfnWndProc = _wndProc, 327 | hCursor = LoadCursor(IntPtr.Zero, 32512 /*IDC_ARROW*/), 328 | }; 329 | 330 | _wndClass = RegisterClass(wc); 331 | 332 | if (_wndClass == 0 && Marshal.GetLastWin32Error() != 0x582) //ERROR_CLASS_ALREADY_EXISTS 333 | throw new Win32Exception(); 334 | } 335 | 336 | /// 337 | /// 创建窗口 338 | /// 339 | void CreateWindow() 340 | { 341 | var exStyle = 0x80000; //WS_EX_LAYERED 342 | 343 | if (TopMost) 344 | exStyle |= 0x8; //WS_EX_TOPMOST 345 | 346 | if (!Activation) 347 | exStyle |= 0x08000000; //WS_EX_NOACTIVATE 348 | 349 | if (MouseThrough) 350 | exStyle |= 0x20; //WS_EX_TRANSPARENT 351 | 352 | if (ShowInTaskbar) 353 | exStyle |= 0x40000; //WS_EX_APPWINDOW 354 | 355 | var style = unchecked((int)0x80000000) //WS_POPUP。不能加WS_VISIBLE,会抢焦点,改用ShowWindow显示 356 | | 0x80000; //WS_SYSMENU 357 | 358 | _hWnd = CreateWindowEx(exStyle, ClassName, null, style, 359 | 0, 0, 0, 0, //坐标尺寸全由UpdateLayeredWindow接管,这里无所谓 360 | IntPtr.Zero, IntPtr.Zero, HInstance, IntPtr.Zero); 361 | 362 | if (_hWnd == IntPtr.Zero) 363 | throw new Win32Exception(); 364 | } 365 | 366 | int DoMessageLoop() 367 | { 368 | var m = new MSG(); 369 | int result; 370 | 371 | while (_continueLoop && (result = GetMessage(ref m, IntPtr.Zero, 0, 0)) != 0) 372 | { 373 | if (result == -1) 374 | return Marshal.GetLastWin32Error(); 375 | 376 | TranslateMessage(ref m); 377 | DispatchMessage(ref m); 378 | } 379 | 380 | return 0; 381 | } 382 | 383 | protected virtual IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam) 384 | { 385 | //Debug.WriteLine((hWnd == _hWnd) + ":0x" + Convert.ToString(msg, 16), "WndProc"); 386 | 387 | switch (msg) 388 | { 389 | case 0x10: //WM_CLOSE 390 | Close(); 391 | break; 392 | case 0x2: //WM_DESTROY 393 | EnableWindow(_activeWindow, true); 394 | _continueLoop = false; 395 | break; 396 | } 397 | 398 | return DefWndProc(hWnd, msg, wParam, lParam); 399 | } 400 | 401 | protected virtual IntPtr DefWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam) => 402 | DefWindowProc(hWnd, msg, wParam, lParam); 403 | 404 | void ShowCore(bool modal) 405 | { 406 | if (IsDisposed) 407 | throw new ObjectDisposedException(Name ?? string.Empty); 408 | 409 | if (_visible) 410 | return; 411 | 412 | if (modal) 413 | { 414 | IsModal = true; 415 | Activation = true; 416 | _activeWindow = GetActiveWindow(); 417 | EnableWindow(_activeWindow, false); 418 | } 419 | 420 | CreateWindow(); 421 | ShowWindow(_hWnd, Activation ? 5 /*SW_SHOW*/ : 8 /*SW_SHOWNA*/); 422 | _visible = true; 423 | 424 | OnShowing(EventArgs.Empty); 425 | 426 | //Showing事件中也许会关闭窗体 427 | if (IsDisposed) 428 | return; 429 | 430 | TryUpdate(); 431 | 432 | if (modal) 433 | { 434 | var result = DoMessageLoop(); 435 | SetActiveWindow(_activeWindow); 436 | if (result != 0) 437 | throw new Win32Exception(result); 438 | } 439 | } 440 | 441 | /// 442 | /// 显示窗体 443 | /// 444 | public void Show() => ShowCore(false); 445 | 446 | /// 447 | /// 显示模式窗体 448 | /// 449 | public void ShowDialog() => ShowCore(true); 450 | 451 | /// 452 | /// 隐藏窗体 453 | /// 454 | public void Hide() => Visible = false; 455 | 456 | /// 457 | /// 挂起更新 458 | /// 459 | public void SuspendLayout() => _layoutSuspended = true; 460 | 461 | /// 462 | /// 取消挂起状态并立即执行一次更新 463 | /// 464 | public void ResumeLayout() => ResumeLayout(true); 465 | 466 | /// 467 | /// 取消挂起状态 468 | /// 469 | /// 是否立即执行一次更新 470 | public void ResumeLayout(bool performLayout) 471 | { 472 | _layoutSuspended = false; 473 | if (performLayout) 474 | Update(); 475 | } 476 | 477 | protected virtual void OnShowing(EventArgs e) => Showing?.Invoke(this, e); 478 | 479 | protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); 480 | 481 | void TryUpdate() 482 | { 483 | if (!_layoutSuspended) 484 | Update(); 485 | } 486 | 487 | /// 488 | /// 更新窗体 489 | /// 490 | /// 491 | public virtual void Update() 492 | { 493 | if (!_visible) 494 | return; 495 | 496 | //后续更新其实在nt6开启桌面主题的情况下,一干参数可以为null, 497 | //但是为了兼容其他情况,还是都指定 498 | if (!UpdateLayeredWindow(_hWnd, 499 | IntPtr.Zero, LocationInternal, SizeInternal, //注意这个尺寸只能小于等于图片尺寸,否则不会显示 500 | _dcMemory, PointOrSize.Empty, 501 | 0, ref _blend, 2 /*ULW_ALPHA*/)) 502 | { 503 | //忽略窗体句柄无效ERROR_INVALID_WINDOW_HANDLE 504 | if (Marshal.GetLastWin32Error() is { } errorCode && errorCode != 0x578) 505 | throw new Win32Exception(errorCode); 506 | } 507 | } 508 | 509 | /// 510 | /// 关闭并销毁窗体。须与Show系方法处于同一线程 511 | /// 512 | public void Close() 513 | { 514 | var e = new CancelEventArgs(); 515 | OnClosing(e); 516 | if (!e.Cancel) 517 | { 518 | _visible = false; 519 | Dispose(); 520 | } 521 | } 522 | 523 | /// 524 | /// 释放窗体。须与Show系方法处于同一线程 525 | /// 526 | public void Dispose() 527 | { 528 | Dispose(true); 529 | GC.SuppressFinalize(this); 530 | } 531 | 532 | protected virtual void Dispose(bool disposing) 533 | { 534 | if (IsDisposed) 535 | return; 536 | 537 | Tag = null; 538 | 539 | //销毁窗体 540 | DestroyWindow(_hWnd); 541 | _hWnd = IntPtr.Zero; 542 | 543 | //注销窗口类 544 | //窗口类是共用的,每个实例都尝试注册和注销 545 | //实际效果就是先开的注册,后关的注销 546 | if (_wndClass != 0) 547 | { 548 | if (UnregisterClass(ClassName, HInstance)) 549 | _wndProc = null; //只有注销成功时才解绑窗口过程 550 | 551 | _wndClass = 0; 552 | } 553 | 554 | if (_oldObj != IntPtr.Zero) 555 | SelectObject(_dcMemory, _oldObj); 556 | 557 | DeleteDC(_dcMemory); 558 | 559 | if (_hBmp != IntPtr.Zero) 560 | DeleteObject(_hBmp); 561 | 562 | if (_defaultHBmp != IntPtr.Zero) 563 | DeleteObject(_defaultHBmp); 564 | 565 | _oldObj = IntPtr.Zero; 566 | _dcMemory = IntPtr.Zero; 567 | _hBmp = IntPtr.Zero; 568 | _defaultHBmp = IntPtr.Zero; 569 | 570 | IsDisposed = true; 571 | } 572 | 573 | ~LayeredWindow() 574 | { 575 | Dispose(false); 576 | } 577 | 578 | //窗口过程委托 579 | delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); 580 | 581 | #region Win32 API 582 | 583 | [DllImport("user32.dll", SetLastError = true)] 584 | static extern IntPtr SetActiveWindow(IntPtr hWnd); 585 | 586 | [DllImport("user32.dll")] 587 | static extern bool EnableWindow(IntPtr hWnd, bool bEnable); 588 | 589 | [DllImport("user32.dll")] 590 | static extern IntPtr GetActiveWindow(); 591 | 592 | [DllImport("user32.dll", SetLastError = true)] 593 | static extern IntPtr LoadCursor(IntPtr hInstance, int iconId); 594 | 595 | [DllImport("user32.dll")] 596 | static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); 597 | 598 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 599 | static extern bool UnregisterClass(string lpClassName, IntPtr hInstance); 600 | 601 | [DllImport("user32.dll")] 602 | static extern IntPtr DefWindowProc(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); 603 | 604 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 605 | static extern short RegisterClass(WNDCLASS wc); 606 | 607 | [DllImport("user32.dll", SetLastError = true)] 608 | static extern int GetMessage(ref MSG msg, IntPtr hWnd, int wMsgFilterMin, int wMsgFilterMax); 609 | 610 | [DllImport("user32.dll")] 611 | static extern IntPtr DispatchMessage(ref MSG msg); 612 | 613 | [DllImport("user32.dll")] 614 | static extern bool TranslateMessage(ref MSG msg); 615 | 616 | [DllImport("gdi32.dll")] 617 | static extern bool DeleteObject(IntPtr hObject); 618 | 619 | [DllImport("gdi32.dll", SetLastError = true)] 620 | static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); 621 | 622 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 623 | static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); 624 | 625 | [DllImport("user32.dll", SetLastError = true)] 626 | static extern bool UpdateLayeredWindow(IntPtr hWnd, IntPtr hdcDst, PointOrSize pptDst, PointOrSize pSizeDst, IntPtr hdcSrc, PointOrSize pptSrc, int crKey, ref BLENDFUNCTION pBlend, int dwFlags); 627 | 628 | [DllImport("gdi32.dll", SetLastError = true)] 629 | static extern IntPtr CreateCompatibleDC(IntPtr hDC); 630 | 631 | [DllImport("gdi32.dll")] 632 | static extern bool DeleteDC(IntPtr hdc); 633 | 634 | [DllImport("user32.dll", SetLastError = true)] 635 | static extern bool DestroyWindow(IntPtr hWnd); 636 | 637 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 638 | class WNDCLASS 639 | { 640 | public int style; 641 | public WndProcDelegate lpfnWndProc; 642 | public int cbClsExtra; 643 | public int cbWndExtra; 644 | public IntPtr hInstance; 645 | public IntPtr hIcon; 646 | public IntPtr hCursor; 647 | public IntPtr hbrBackground; 648 | public string lpszMenuName; 649 | public string lpszClassName; 650 | } 651 | 652 | [StructLayout(LayoutKind.Sequential)] 653 | struct BLENDFUNCTION 654 | { 655 | public byte BlendOp; 656 | public byte BlendFlags; 657 | public byte SourceConstantAlpha; 658 | public byte AlphaFormat; 659 | } 660 | 661 | [StructLayout(LayoutKind.Sequential)] 662 | struct MSG 663 | { 664 | public IntPtr HWnd; 665 | public int Message; 666 | public IntPtr WParam; 667 | public IntPtr LParam; 668 | public int Time; 669 | public int X; 670 | public int Y; 671 | } 672 | 673 | [StructLayout(LayoutKind.Sequential)] 674 | class PointOrSize 675 | { 676 | public int XOrWidth, YOrHeight; 677 | 678 | public static readonly PointOrSize Empty = new(); 679 | 680 | public PointOrSize() 681 | { 682 | XOrWidth = 0; 683 | YOrHeight = 0; 684 | } 685 | 686 | public PointOrSize(int xOrWidth, int yOrHeight) 687 | { 688 | XOrWidth = xOrWidth; 689 | YOrHeight = yOrHeight; 690 | } 691 | } 692 | 693 | #endregion 694 | } -------------------------------------------------------------------------------- /MessageTip/MessageTip.v1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using System.Windows.Forms; 8 | 9 | namespace AhDung.WinForm 10 | { 11 | /// 12 | /// 内置图标枚举 13 | /// 14 | public enum TipIcon 15 | { 16 | /// 17 | /// 无图标 18 | /// 19 | None, 20 | /// 21 | /// 良好。绿勾 √ 22 | /// 23 | Ok, 24 | /// 25 | /// 警告。黄色感叹号 ! 26 | /// 27 | Warning, 28 | /// 29 | /// 错误。红叉 × 30 | /// 31 | Error 32 | } 33 | 34 | /// 35 | /// 轻快型消息框 36 | /// 37 | public static class MessageTip 38 | { 39 | /// 40 | /// 内置图标数组,顺序与TipIcon枚举对应 41 | /// 42 | static readonly Image[] _icons; 43 | 44 | /// 45 | /// 全局停留时长(毫秒),影响后续弹出的tip。默认500 46 | /// 47 | public static int DefaultDelay { get; set; } 48 | 49 | /// 50 | /// 是否允许上浮动画。默认true 51 | /// 52 | public static bool AllowFloating { get; set; } 53 | 54 | static MessageTip() 55 | { 56 | DefaultDelay = 500; 57 | AllowFloating = true; 58 | 59 | using (var ms = new MemoryStream(Convert.FromBase64String(DefaultIconData))) 60 | { 61 | var spriteImage = (Bitmap)Image.FromStream(ms); 62 | 63 | using (spriteImage) 64 | { 65 | #if SmallSize 66 | const int imageSize = 24; 67 | #else 68 | const int imageSize = 32; 69 | #endif 70 | _icons = new Image[4]; //[0]=null 71 | _icons[1] = spriteImage.Clone(new Rectangle(0, 0, imageSize, imageSize), spriteImage.PixelFormat); 72 | _icons[2] = spriteImage.Clone(new RectangleF(imageSize, 0, imageSize, imageSize), spriteImage.PixelFormat); 73 | _icons[3] = spriteImage.Clone(new RectangleF(2 * imageSize, 0, imageSize, imageSize), spriteImage.PixelFormat); 74 | } 75 | } 76 | } 77 | 78 | /// 79 | /// 在指定控件附近显示良好消息,图标为绿勾 √。与传入TipIcon.Ok等价 80 | /// 81 | /// 控件或工具栏项 82 | /// 消息文本 83 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 84 | public static void ShowOk(Component controlOrItem, string text = null, int delay = -1) 85 | { 86 | Show(controlOrItem, text, _icons[1], delay); 87 | } 88 | 89 | /// 90 | /// 显示良好消息,图标为绿勾 √。与传入TipIcon.Ok等价 91 | /// 92 | /// 消息文本 93 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 94 | /// 指定显示位置 95 | /// 以point为中心显示 96 | public static void ShowOk(string text = null, int delay = -1, Point? point = null, bool centerByPoint = false) 97 | { 98 | Show(text, _icons[1], delay, point, centerByPoint); 99 | } 100 | 101 | /// 102 | /// 在指定控件附近显示警告消息,图标为黄色感叹号 !。与传入TipIcon.Warning等价 103 | /// 104 | /// 控件或工具栏项 105 | /// 消息文本 106 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 107 | public static void ShowWarning(Component controlOrItem, string text = null, int delay = -1) 108 | { 109 | Show(controlOrItem, text, _icons[2], delay); 110 | } 111 | 112 | /// 113 | /// 显示警告消息,图标为黄色感叹号 !。与传入TipIcon.Warning等价 114 | /// 115 | /// 消息文本 116 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 117 | /// 指定显示位置 118 | /// 以point为中心显示 119 | public static void ShowWarning(string text = null, int delay = -1, Point? point = null, bool centerByPoint = false) 120 | { 121 | Show(text, _icons[2], delay, point, centerByPoint); 122 | } 123 | 124 | /// 125 | /// 在指定控件附近显示出错消息,图标为红叉 X。与传入TipIcon.Error等价 126 | /// 127 | /// 控件或工具栏项 128 | /// 消息文本 129 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 130 | public static void ShowError(Component controlOrItem, string text = null, int delay = -1) 131 | { 132 | Show(controlOrItem, text, _icons[3], delay); 133 | } 134 | 135 | /// 136 | /// 显示出错消息,图标为红叉 X。与传入TipIcon.Error等价 137 | /// 138 | /// 消息文本 139 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 140 | /// 指定显示位置 141 | /// 以point为中心显示 142 | public static void ShowError(string text = null, int delay = -1, Point? point = null, bool centerByPoint = false) 143 | { 144 | Show(text, _icons[3], delay, point, centerByPoint); 145 | } 146 | 147 | /// 148 | /// 在指定控件附近显示消息 149 | /// 150 | /// 控件或工具栏项 151 | /// 消息文本 152 | /// 内置图标 153 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 154 | public static void Show(Component controlOrItem, string text, TipIcon tipIcon = TipIcon.None, int delay = -1) 155 | { 156 | if (controlOrItem == null) 157 | { 158 | throw new ArgumentNullException("controlOrItem"); 159 | } 160 | Show(text, CheckAndConvertTipIconValue(tipIcon), delay, GetCenterPosition(controlOrItem), !(controlOrItem is ButtonBase || controlOrItem is ToolStripItem)); 161 | } 162 | 163 | /// 164 | /// 在指定控件附近显示消息 165 | /// 166 | /// 控件或工具栏项 167 | /// 消息文本 168 | /// 图标 169 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 170 | public static void Show(Component controlOrItem, string text, Image icon, int delay = -1) 171 | { 172 | if (controlOrItem == null) 173 | { 174 | throw new ArgumentNullException("controlOrItem"); 175 | } 176 | Show(text, icon, delay, GetCenterPosition(controlOrItem), !(controlOrItem is ButtonBase || controlOrItem is ToolStripItem)); 177 | } 178 | 179 | /// 180 | /// 显示消息 181 | /// 182 | /// 消息文本 183 | /// 内置图标 184 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 185 | /// 指定显示位置。为null则按活动控件 186 | /// 以point为中心显示 187 | public static void Show(string text, TipIcon tipIcon = TipIcon.None, int delay = -1, Point? point = null, bool centerByPoint = false) 188 | { 189 | Show(text, CheckAndConvertTipIconValue(tipIcon), delay, point, centerByPoint); 190 | } 191 | 192 | /// 193 | /// 显示消息 194 | /// 195 | /// 消息文本 196 | /// 图标 197 | /// 消息停留时长(毫秒)。指定负数则使用 DefaultDelay 198 | /// 指定显示位置。为null则按活动控件 199 | /// 以point为中心显示 200 | public static void Show(string text, Image icon, int delay = -1, Point? point = null, bool centerByPoint = false) 201 | { 202 | if (point == null) 203 | { 204 | //确定基准点 205 | var focusControl = Control.FromHandle(NativeMethods.GetFocus()); 206 | 207 | if (focusControl is TextBoxBase)//若焦点是文本框,取光标位置 208 | { 209 | var pt = GetCaretPosition(); 210 | pt.Y += focusControl.Font.Height / 2; 211 | point = focusControl.PointToScreen(pt); 212 | } 213 | else if (focusControl is ButtonBase)//若焦点是按钮,取按钮中心点 214 | { 215 | point = GetCenterPosition(focusControl); 216 | } 217 | else //其余情况在鼠标附近显示 218 | { 219 | point = Control.MousePosition; 220 | } 221 | } 222 | 223 | //异步Show。线程池偶尔不可靠 224 | new Thread(() => new TipForm 225 | { 226 | TipText = text, 227 | TipIcon = icon, 228 | Delay = delay < 0 ? DefaultDelay : delay, 229 | Floating = AllowFloating, 230 | BasePoint = point.Value, 231 | CenterByBasePoint = centerByPoint 232 | }.ShowDialog())//要让创建浮动窗体的线程具有消息循环,所以要用ShowDialog 233 | { IsBackground = true }.Start(); 234 | } 235 | 236 | /// 237 | /// 检测枚举值合法性并转换为Image 238 | /// 239 | private static Image CheckAndConvertTipIconValue(TipIcon tipIcon) 240 | { 241 | int i = (int)tipIcon; 242 | if (i < 0 || i > 3) 243 | { 244 | throw new InvalidEnumArgumentException("tipIcon", i, typeof(TipIcon)); 245 | } 246 | return _icons[i]; 247 | } 248 | 249 | /// 250 | /// 获取控件中心点的屏幕坐标 251 | /// 252 | private static Point GetCenterPosition(Component controlOrItem) 253 | { 254 | Control c = controlOrItem as Control; 255 | if (c != null) 256 | { 257 | return c.PointToScreen(new Point(c.Width / 2, c.Height / 2)); 258 | } 259 | var item = controlOrItem as ToolStripItem; 260 | if (item != null) 261 | { 262 | var pos = item.Bounds.Location; 263 | pos.X += item.Width / 2; 264 | pos.Y += item.Height / 2; 265 | return item.Owner.PointToScreen(pos); 266 | } 267 | throw new ArgumentException(); 268 | } 269 | 270 | /// 271 | /// 获取输入光标位置,文本框内坐标 272 | /// 273 | private static Point GetCaretPosition() 274 | { 275 | Point pt; 276 | NativeMethods.GetCaretPos(out pt); 277 | return pt; 278 | } 279 | 280 | /// 281 | /// 内置图标数据:√ ! X 282 | /// 283 | /// GIF文件 284 | const string DefaultIconData = 285 | #if SmallSize 286 | @"R0lGODlhSAAYAMQAAOjbK9eZmejy4uVdXSUkELHetssRESmbM1XYZfPqk9XHZdbROrk1NcSZmfbx 287 | vhbFJqeXMmtrWzawRPPkdODTaLMSEoozM8u9St/WTr+0LuDXm0vGWdy3t4nFjqETE////ywAAAAA 288 | SAAYAAAF/+AnjmRpnmiqrmzrviPHBGzAcHApSEXrYAoHSzaYqWxFXO4j2CB4LEoiQVkRB0XaCYm9 289 | LZ2IZy/loDjKwhQDyzaWuO1cBxyWqKRT6nFd7GpFXHxuLgUSYXVjJ1QOCRNlCXt9a25IDJODLYVh 290 | GxsSHSoKUxMTUworlZZZqJaYLDtOnBIbKhpSGhERoRQap5esv6x/La8IDw8SEgIpAhiluBqkCsqR 291 | wMDCJIWfJZ2dxwfTKLWNCrgKpBi8Kw3Vv9cjmp4kHYYH3oknAheNE+S5FKQX0qhYZ6mCh4MMGqCA 292 | 90RbhwMI6kk4oC0FBSkTLhIgcGHBAgoK0h2xwOCgSQvuRP8YQkavw7wn3mapcKBvAgYMFzYu6Pgx 293 | 4ECSBk0eRHniIYJunQ5sMPbgQLIVCv5h8LgAQgSqC4CYQkGwgkGvHrxWsKDQxLylTGE2/baCZoKp 294 | VEdhzerTRAMLYr1awJuXqFmlTAMfuIciKlyPGTZmAEAVwIWtJO4a6NsgAN8Kk8kWrRe4acUUGi5M 295 | AECaNISNEUozdizyg2QDsA1odm0BNmbZZUs8FPw5xQUMqk0rDs74wojXsWeLQA5buTzOTcGBzgCc 296 | +CjipDOkqx0bt13uyVHsjiA9BYTq2NMzhiCCA3jnkd8r2UwYNIT7+PPr1y/SvfcUklkw3xIEtuBe 297 | bgAKWOAFggyyEAIAOw=="; 298 | #else 299 | @"R0lGODlhYAAgANUAAOrcJ9LORebm5tJKShPLJLczM/z3s/XrkNfSOhS2JKaYMezeaMoREfhwcNS3 300 | t7IREVSkWpWQZjCpPfz8+zS4RN3PZdTU1EjLWG9uaO/w7/Dke1HVYfHsx5UzM8Q8PLjYuqmmn8bl 301 | yeO3txsbD+fck9DAUeDaVV1cHuHXMDGTOO3iSeHy4+BYWDvGTPn25O/hN5PIl8K1Pbfhu/z78nW/ 302 | fPb39lbeZ5/WpIAzM2HpcqITExGiIjV/N8S9luLbqf///yH5BAAAAAAALAAAAABgACAAAAb/wJ9w 303 | SCwaj8ikcslsOp/QKFNUEEGpVqm2OQtUZlJqo+oUk7fHFUWmJWk0pKi4Mc4qxaw6uli7bNZRMxUG 304 | BhUuT3gseWdIc4p6e0I0Fzk5EmxPboQHcU2Jiot2RZ+PjFs3LTk2G5aYTC6DBweFh0wFDaC5kERi 305 | Ayy+v4oFeyEUNqsbrJdNmm8GcJ63A9PUA7s/VL/V0yymUWobNhfj4xsSTByxb7MVHNEN29xn2fG+ 306 | 3lB9F+LkfstLFbMWaBAoq0KZWx4SJhzgoZuIbB4YLox4DwqNFuJaaGxxgQIMdIM0RMCAIYKGQu6a 307 | OCjAQqHLAQUKSHSZsOKTG8YubNRIgUYT/4AHFpAkqaHghIMtPRRQuhAm06VQHSiRAQhJCAl/JCSQ 308 | wJUChBogDQgcimHBggPtnqyEGbOt27c2hciQYMNfkRUSOu7Yu1fCVyYTAA5cMJKk2aJfEHVgC9et 309 | UqlJqNowZlfIBAgULvDtK2FFEx+DzArFcCKCaLQ+rixVCpd1B1FG5k6mQNnVDxoUNmzewTUElxIH 310 | BhMeMeJEiQBeFpQAo7YDa9YPdEiP+0M27evmMN2QQCNBgs0SbjgBHXSBCQQniJ9AjnxBhU4H2Uqf 311 | L/21ErrXrydoIeHD1Q3e7eDdVh814QJwGpjAngLEKcBeeyWA5cli9FVYH2xEzPUHBS0MuP+fBClc 312 | 4KGA3B3FTAUaBICAigEocMIJCqy4ohcBwHeHczpEZyF99iGhIQUeetfCH0H6lYETByaIwJJLxvBi 313 | DEwyaUIJtSThQAcFPBCdljl2uWV0PR7xQV4EDFhmAmWe6R0EnjlRQQULRLmkCW+cJ6eCBiUhApZa 314 | 9unnn312AJmYErRAwKFopplmAin45gQHJWgg56SUTpmSEVcyAKiWHXSwKaeDGjGmooeWemgKHzwR 315 | mAkmoODqqygwOIICsL66Yp6j4KDpA7vuKuiVWu7KK6hJfJCCqcimUOCjJSxQ66vEEffsqyVcKoQD 316 | umqqrbY4QJYprwxs+4CgxR6LLAEp+KT/agkmqAArANBG+yy8KARQgok/YBvuvvx2S4S+4PLLgL9I 317 | GIvsDhDg+1kM7gLgMLwPR0BcBA8/jMLDMaQmRLYCh0vwvxx3jIMSBiMKwZGqxmDCCxW3rIIsKrTc 318 | MgIxmKivyKGC3PHAOf8wQQ0ZZCAACObyYIEAQWdQw9ITNH2EDwzLLPXUFWc8xM37fnwE1h73/APQ 319 | AlggNgg88NCD2EcLgLTSThtB6wsqvCD33HIDwDLLVDsc678dZO11EZl2jcTPQattuNpJK11D020T 320 | MUEPCkQu+eSUV255DwrrS65Kum4eyRJNAy304aSXXrriTk8wOgg4gIB22okbjjbrICC9E/jnuEcR 321 | +uiHB73076KT7rvTQQAAOw=="; 322 | #endif 323 | 324 | /// 325 | /// 浮动消息层 326 | /// 327 | private class TipForm : Form 328 | { 329 | /// 330 | /// 图标和文本之间的间距(像素) 331 | /// 332 | const int IconTextSpacing = 3; 333 | 334 | /// 335 | /// 是否向下浮动 336 | /// 337 | bool _floatDown; 338 | 339 | /// 340 | /// 基准点。用于指导本窗体显示位置 341 | /// 342 | public Point BasePoint { get; set; } 343 | 344 | /// 345 | /// 是否以基准点为中心显示。false则会在基准点上下错开一定距离 346 | /// 347 | public bool CenterByBasePoint { get; set; } 348 | 349 | Image _icon; 350 | /// 351 | /// 提示图标 352 | /// 353 | public Image TipIcon 354 | { 355 | private get { return _icon; } 356 | 357 | //让每个窗体拥有各自的Image对象,共用有可能造成争用异常 358 | //在窗体销毁时一同释放_icon 359 | set 360 | { 361 | if (_icon == value) { return; } 362 | if (_icon != null) { _icon.Dispose(); } 363 | _icon = value == null ? null : new Bitmap(value); 364 | } 365 | } 366 | 367 | string _tipText; 368 | /// 369 | /// 提示文本 370 | /// 371 | public string TipText 372 | { 373 | get { return _tipText ?? string.Empty; } 374 | set { _tipText = value; } 375 | } 376 | 377 | /// 378 | /// 停留时长(毫秒) 379 | /// 380 | [DefaultValue(500)] 381 | public int Delay { get; set; } 382 | 383 | /// 384 | /// 是否浮动 385 | /// 386 | [DefaultValue(true)] 387 | public bool Floating { get; set; } 388 | 389 | //显示后不激活,即不抢焦点 390 | protected override bool ShowWithoutActivation 391 | { 392 | get { return true; } 393 | } 394 | 395 | public TipForm() 396 | { 397 | //双缓冲。有必要 398 | SetStyle(ControlStyles.UserPaint, true); 399 | DoubleBuffered = true; 400 | 401 | InitializeComponent(); 402 | 403 | Delay = 500; 404 | Floating = true; 405 | 406 | this._timer.Tick += timer_Tick; 407 | this.Load += TipForm_Load; 408 | this.Shown += TipForm_Shown; 409 | this.FormClosing += TipForm_FormClosing; 410 | } 411 | 412 | /// 413 | /// 根据图标和文字处理窗体尺寸 414 | /// 415 | private void ProcessClientSize() 416 | { 417 | Size size = Size.Empty; 418 | if (TipIcon != null) 419 | { 420 | size += TipIcon.Size; 421 | } 422 | if (TipText.Length != 0) 423 | { 424 | if (TipIcon != null) 425 | { 426 | size.Width += IconTextSpacing; 427 | } 428 | var textSize = TextRenderer.MeasureText(TipText, this.Font); 429 | size.Width += textSize.Width; 430 | if (size.Height < textSize.Height) { size.Height = textSize.Height; } 431 | } 432 | this.ClientSize = size + Padding.Size; 433 | } 434 | 435 | /// 436 | /// 根据基准点处理窗体显示位置 437 | /// 438 | private void ProcessLocation() 439 | { 440 | var p = BasePoint; 441 | p.X -= this.Width / 2; 442 | 443 | //以基准点所在屏为界 444 | var screen = Screen.FromPoint(BasePoint).Bounds; 445 | 446 | //横向处理。距离屏幕左右两边太近时的处理 447 | //多屏下left可能为负,所以right = width - (-left) = width + left 448 | int dist = 10; //至少距离边缘多少像素 449 | int left, right; 450 | if (p.X < (left = screen.Left + dist)) 451 | { 452 | p.X = left; 453 | } 454 | else if (p.X > (right = screen.Width + screen.Left - dist - this.Width)) 455 | { 456 | p.X = right; 457 | } 458 | 459 | //纵向处理 460 | if (CenterByBasePoint) 461 | { 462 | p.Y -= this.Height / 2; 463 | } 464 | else 465 | { 466 | dist = 20;//错开基准点上下20像素 467 | p.Y -= this.Height + dist; 468 | } 469 | 470 | if (p.Y < screen.Top + 50)//若太靠屏幕顶部 471 | { 472 | if (!CenterByBasePoint) 473 | { 474 | p.Y += this.Height + 2 * dist;//在下方错开 475 | } 476 | 477 | _floatDown = true;//动画改为下降 478 | } 479 | 480 | this.Location = p; 481 | } 482 | 483 | void TipForm_Load(object sender, EventArgs e) 484 | { 485 | //这俩顺序不能乱 486 | ProcessClientSize(); 487 | ProcessLocation(); 488 | 489 | //浮动动画。采用异步,以不阻塞透明渐变动画的进行 490 | if (Floating) 491 | { 492 | new Thread(() => //用线程池偶尔会忙不过来 493 | { 494 | int adj = _floatDown ? 1 : -1; 495 | while (this.IsHandleCreated) 496 | { 497 | this.BeginInvoke(new Action(arg => 498 | { 499 | this.Top += adj; 500 | Application.DoEvents(); 501 | }), (object)null); 502 | 503 | Thread.Sleep(30); 504 | } 505 | }) { IsBackground = true }.Start(); 506 | } 507 | 508 | //透明渐入动画。之所以不用异步是为了在完全显示后再开始Delay的计时 509 | //不然如果Delay设置过低,还没等看清就渐隐了 510 | this.Opacity = 0; 511 | while (this.Opacity < 1) 512 | { 513 | this.Opacity += 0.1; 514 | Application.DoEvents(); 515 | Thread.Sleep(10); 516 | } 517 | } 518 | 519 | void TipForm_Shown(object sender, EventArgs e) 520 | { 521 | //timer.Interval不能为0 522 | if (Delay > 0) 523 | { 524 | _timer.Interval = Delay; 525 | _timer.Start(); 526 | } 527 | else 528 | { 529 | this.Close(); 530 | } 531 | } 532 | 533 | void timer_Tick(object sender, EventArgs e) 534 | { 535 | _timer.Stop(); 536 | this.Close(); 537 | } 538 | 539 | void TipForm_FormClosing(object sender, FormClosingEventArgs e) 540 | { 541 | //透明渐隐动画 542 | while (this.Opacity > 0) 543 | { 544 | this.Opacity -= 0.1; 545 | Application.DoEvents(); 546 | Thread.Sleep(20); 547 | } 548 | } 549 | 550 | protected override void OnPaint(PaintEventArgs e) 551 | { 552 | base.OnPaint(e); 553 | 554 | var clip = GetPaddedRectangle();//得到作图区域 555 | var g = e.Graphics; 556 | 557 | //画图标 558 | if (TipIcon != null) 559 | { 560 | g.DrawImageUnscaled(TipIcon, clip.Location); 561 | } 562 | //画文本 563 | if (TipText.Length != 0) 564 | { 565 | if (TipIcon != null) 566 | { 567 | clip.X += TipIcon.Width + IconTextSpacing; 568 | } 569 | TextRenderer.DrawText(g, TipText, this.Font, clip, this.ForeColor, TextFormatFlags.VerticalCenter); 570 | } 571 | } 572 | 573 | protected override void OnPaintBackground(PaintEventArgs e) 574 | { 575 | base.OnPaintBackground(e); 576 | 577 | //画边框 578 | ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, SystemColors.ControlDark, ButtonBorderStyle.Solid); 579 | } 580 | 581 | /// 582 | /// 获取刨去Padding的内容区 583 | /// 584 | private Rectangle GetPaddedRectangle() 585 | { 586 | Rectangle r = this.ClientRectangle; 587 | r.X += this.Padding.Left; 588 | r.Y += this.Padding.Top; 589 | r.Width -= this.Padding.Horizontal; 590 | r.Height -= this.Padding.Vertical; 591 | return r; 592 | } 593 | 594 | #region 设计器内容 595 | 596 | protected override void Dispose(bool disposing) 597 | { 598 | if (disposing) 599 | { 600 | _timer.Dispose();//注意释放这货 601 | if (_icon != null) { _icon.Dispose(); } 602 | } 603 | base.Dispose(disposing); 604 | } 605 | 606 | private void InitializeComponent() 607 | { 608 | this._timer = new System.Windows.Forms.Timer(); 609 | this.SuspendLayout(); 610 | 611 | this.AutoScaleMode = AutoScaleMode.None; 612 | this.BackColor = Color.White; 613 | #if SmallSize 614 | this.Font = SystemFonts.MessageBoxFont; 615 | this.Padding = new Padding(10, 5, 10, 5); 616 | #else 617 | this.Font = new Font(SystemFonts.MessageBoxFont.FontFamily, 12); 618 | this.Padding = new Padding(20, 10, 20, 10); 619 | #endif 620 | this.FormBorderStyle = FormBorderStyle.None; 621 | this.Name = "TipForm"; 622 | this.ShowInTaskbar = false; 623 | 624 | this.ResumeLayout(false); 625 | } 626 | 627 | private System.Windows.Forms.Timer _timer; 628 | 629 | #endregion 630 | } 631 | 632 | /// 633 | /// Win32 API 634 | /// 635 | private static class NativeMethods 636 | { 637 | [DllImport("User32.dll", SetLastError = true)] 638 | public static extern bool GetCaretPos(out Point pt); 639 | 640 | [DllImport("user32.dll")] 641 | public static extern IntPtr GetFocus(); 642 | } 643 | } 644 | } 645 | --------------------------------------------------------------------------------