├── .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 | 
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