├── Line
├── LineForm.cs
├── LineIco.ico
├── Line.csproj
├── VerticalLineForm.resx
├── HorizontalLineForm.resx
├── MonitoredAppsWindow.cs
├── VerticalLineForm.cs
├── HorizontalLineForm.cs
└── BoundingBoxForm.cs
├── Line_wpf
├── LineIco.ico
├── App.xaml
├── MainWindow.xaml
├── BoundingBoxWindow.xaml
├── VerticalLineWindow.xaml
├── HorizontalLineWindow.xaml
├── Line_wpf.csproj
├── Program.cs
├── MonitoredAppsWindow.xaml
├── App.xaml.cs
└── MonitoredAppsWindow.xaml.cs
├── Line.Tests
├── Line.Tests.csproj
└── ConfigSaveTests.cs
├── LICENSE.txt
├── LICENSE
├── Line.sln
├── Program.cs
├── .gitattributes
├── README.md
└── .gitignore
/Line/LineForm.cs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Line/LineIco.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onlyclxy/Line/master/Line/LineIco.ico
--------------------------------------------------------------------------------
/Line_wpf/LineIco.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onlyclxy/Line/master/Line_wpf/LineIco.ico
--------------------------------------------------------------------------------
/Line_wpf/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Line.Tests/Line.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | false
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Line_wpf/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Line/Line.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WinExe
6 | net8.0-windows
7 | true
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Always
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Line_wpf/BoundingBoxWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Line_wpf/VerticalLineWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Line_wpf/HorizontalLineWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Line_wpf/Line_wpf.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0-windows
6 | true
7 | true
8 | enable
9 | disable
10 | false
11 | false
12 | LineIco.ico
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Always
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Screen Reference Line Tool
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Line.Tests/ConfigSaveTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using System.Runtime.Serialization;
5 | using Xunit;
6 |
7 | namespace Line.Tests
8 | {
9 | public class ConfigSaveTests
10 | {
11 | private static void InvokeSave(object instance, string methodName)
12 | {
13 | var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
14 | method!.Invoke(instance, null);
15 | }
16 |
17 | private static string GetConfigPath(object instance)
18 | {
19 | var field = instance.GetType().GetField("configPath", BindingFlags.NonPublic | BindingFlags.Instance);
20 | return (string)field!.GetValue(instance)!;
21 | }
22 |
23 | private static void TestSaveConfig(Type type)
24 | {
25 | var instance = FormatterServices.GetUninitializedObject(type);
26 | var path = GetConfigPath(instance);
27 | var dir = Path.GetDirectoryName(path)!;
28 | if (Directory.Exists(dir))
29 | {
30 | Directory.Delete(dir, true);
31 | }
32 |
33 | InvokeSave(instance, "SaveConfig");
34 |
35 | Assert.True(File.Exists(path));
36 | }
37 |
38 | [Fact]
39 | public void LineForm_SaveConfig_CreatesFile()
40 | {
41 | TestSaveConfig(typeof(Line.LineForm));
42 | }
43 |
44 | [Fact]
45 | public void VerticalLineForm_SaveConfig_CreatesFile()
46 | {
47 | TestSaveConfig(typeof(Line.VerticalLineForm));
48 | }
49 |
50 | [Fact]
51 | public void HorizontalLineForm_SaveConfig_CreatesFile()
52 | {
53 | TestSaveConfig(typeof(Line.HorizontalLineForm));
54 | }
55 |
56 | [Fact]
57 | public void BoundingBoxForm_SaveConfig_CreatesFile()
58 | {
59 | TestSaveConfig(typeof(Line.BoundingBoxForm));
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Line.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34902.65
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Line", "Line\Line.csproj", "{2DC48F3A-58E9-4600-8280-D6A88F4F9DF4}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Line_wpf", "Line_wpf\Line_wpf.csproj", "{E85B9848-33D5-4485-B729-DBBC2E6D6918}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Line.Tests", "Line.Tests\Line.Tests.csproj", "{33E28D51-9686-4CBC-BC38-A956A4D2F569}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {2DC48F3A-58E9-4600-8280-D6A88F4F9DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {2DC48F3A-58E9-4600-8280-D6A88F4F9DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {2DC48F3A-58E9-4600-8280-D6A88F4F9DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {2DC48F3A-58E9-4600-8280-D6A88F4F9DF4}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {E85B9848-33D5-4485-B729-DBBC2E6D6918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {E85B9848-33D5-4485-B729-DBBC2E6D6918}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {E85B9848-33D5-4485-B729-DBBC2E6D6918}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {E85B9848-33D5-4485-B729-DBBC2E6D6918}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {33E28D51-9686-4CBC-BC38-A956A4D2F569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {33E28D51-9686-4CBC-BC38-A956A4D2F569}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {33E28D51-9686-4CBC-BC38-A956A4D2F569}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {33E28D51-9686-4CBC-BC38-A956A4D2F569}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {71200D44-B14E-4C01-834D-6BD79FF9E5E6}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using System.IO;
5 | using System.Text.Json;
6 | using System.Windows.Forms;
7 |
8 | namespace Snipaste
9 | {
10 | public partial class Program : Form
11 | {
12 | // 加载配置
13 | private void LoadConfig()
14 | {
15 | try
16 | {
17 | if (File.Exists(configPath))
18 | {
19 | string jsonString = File.ReadAllText(configPath);
20 | var config = JsonSerializer.Deserialize(jsonString);
21 |
22 | showOnAllScreens = config.ShowOnAllScreens;
23 | lineHeight = config.LineHeight;
24 | lineColor = ColorTranslator.FromHtml(config.LineColor);
25 | lineOpacity = config.LineOpacity;
26 | displayDuration = config.DisplayDuration;
27 | currentHotKey = config.HotKey;
28 | persistentTopmost = config.PersistentTopmost;
29 |
30 | // 新增:加载置顶策略配置
31 | currentTopmostStrategy = (TopmostStrategy)config.TopmostStrategy;
32 | currentTimerInterval = config.TimerInterval;
33 |
34 | // 验证定时器间隔,确保不为0或负数
35 | if (currentTimerInterval <= 0)
36 | {
37 | currentTimerInterval = 100; // 默认值
38 | }
39 |
40 | if (config.MonitoredApplications != null && config.MonitoredApplications.Count > 0)
41 | {
42 | monitoredApplications = config.MonitoredApplications;
43 | }
44 | }
45 | }
46 | catch (Exception ex)
47 | {
48 | // 如果加载失败,使用默认值
49 | showOnAllScreens = false;
50 | lineHeight = 1;
51 | lineColor = Color.Red;
52 | lineOpacity = 100;
53 | displayDuration = 1.5;
54 | currentHotKey = Keys.F5;
55 | persistentTopmost = false;
56 | currentTopmostStrategy = TopmostStrategy.ForceTimer;
57 | currentTimerInterval = 100;
58 | monitoredApplications = new List { "Paster - Snipaste", "PixPin" };
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/Line_wpf/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Threading;
5 | using System.Windows;
6 | using MessageBox = System.Windows.MessageBox;
7 |
8 | namespace Line_wpf
9 | {
10 | internal class Program
11 | {
12 | // 用于确保应用程序只运行一个实例的互斥体
13 | private static readonly Mutex SingleInstanceMutex = new Mutex(true, "LineAppSingleInstanceMutex_WPF");
14 |
15 | // 添加应用程序数据目录路径
16 | private static readonly string AppDataPath = Path.Combine(
17 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
18 | "ScreenLine"
19 | );
20 |
21 | ///
22 | /// 释放单实例互斥体(用于重启功能)
23 | ///
24 | public static void ReleaseSingleInstanceMutex()
25 | {
26 | try
27 | {
28 | SingleInstanceMutex.ReleaseMutex();
29 | }
30 | catch (Exception)
31 | {
32 | // 忽略释放互斥体时的异常
33 | }
34 | }
35 |
36 | ///
37 | /// 检查是否有其他实例正在运行(备用方法)
38 | ///
39 | public static bool IsAnotherInstanceRunning()
40 | {
41 | try
42 | {
43 | var currentProcess = Process.GetCurrentProcess();
44 | string currentProcessName = currentProcess.ProcessName;
45 | Process[] processes = Process.GetProcessesByName(currentProcessName);
46 |
47 | // 检查是否有其他进程具有相同的可执行文件路径
48 | string currentPath = currentProcess.MainModule.FileName;
49 |
50 | foreach (var process in processes)
51 | {
52 | if (process.Id != currentProcess.Id &&
53 | !process.HasExited &&
54 | process.MainModule.FileName.Equals(currentPath, StringComparison.OrdinalIgnoreCase))
55 | {
56 | return true;
57 | }
58 | }
59 |
60 | return false;
61 | }
62 | catch (Exception)
63 | {
64 | // 如果检查失败,返回false(假设没有其他实例)
65 | return false;
66 | }
67 | }
68 |
69 | public static Mutex GetSingleInstanceMutex()
70 | {
71 | return SingleInstanceMutex;
72 | }
73 |
74 | public static string GetAppDataPath()
75 | {
76 | return AppDataPath;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Line_wpf/MonitoredAppsWindow.xaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
26 |
27 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
49 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Line_wpf/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Windows;
5 | using MessageBox = System.Windows.MessageBox;
6 | using System.Runtime.InteropServices;
7 |
8 | namespace Line_wpf
9 | {
10 | ///
11 | /// Interaction logic for App.xaml
12 | ///
13 | public partial class App : System.Windows.Application
14 | {
15 | // 设置DPI感知模式,确保1像素就是1物理像素
16 | [DllImport("user32.dll")]
17 | private static extern bool SetProcessDPIAware();
18 |
19 | [DllImport("shcore.dll")]
20 | private static extern int SetProcessDpiAwareness(int value);
21 |
22 | // DPI感知模式
23 | private enum DPI_AWARENESS
24 | {
25 | DPI_AWARENESS_INVALID = -1,
26 | DPI_AWARENESS_UNAWARE = 0, // DPI无关
27 | DPI_AWARENESS_SYSTEM_AWARE = 1, // 系统DPI感知
28 | DPI_AWARENESS_PER_MONITOR_AWARE = 2 // 每个显示器DPI感知
29 | }
30 |
31 | // 重启模式标志
32 | private bool isRestartMode = false;
33 |
34 | protected override void OnStartup(StartupEventArgs e)
35 | {
36 | // 设置为DPI无关模式,确保1像素就是1物理像素(模拟WinForms的AutoScaleMode.None)
37 | try
38 | {
39 | // 首先尝试使用Windows 8.1+的API
40 | SetProcessDpiAwareness((int)DPI_AWARENESS.DPI_AWARENESS_UNAWARE);
41 | }
42 | catch
43 | {
44 | try
45 | {
46 | // 如果失败,使用Windows Vista+的API
47 | SetProcessDPIAware();
48 | }
49 | catch
50 | {
51 | // 如果都失败,继续运行但可能有DPI问题
52 | }
53 | }
54 |
55 | // 检查是否是重启启动(带特殊参数)
56 | isRestartMode = e.Args.Length > 0 && e.Args[0] == "--restart";
57 |
58 | if (isRestartMode)
59 | {
60 | Console.WriteLine("[启动] 检测到重启模式,将延迟注册快捷键");
61 | }
62 |
63 | // 只有在非重启模式下才检查单实例
64 | if (!isRestartMode)
65 | {
66 | // 使用更可靠的单实例检查
67 | var mutex = Program.GetSingleInstanceMutex();
68 | bool mutexAcquired = false;
69 |
70 | try
71 | {
72 | // 尝试获取互斥锁,等待最多1秒
73 | mutexAcquired = mutex.WaitOne(TimeSpan.FromSeconds(1), false);
74 |
75 | if (!mutexAcquired)
76 | {
77 | // 互斥锁获取失败,说明已有实例在运行
78 | MessageBox.Show("程序已经在运行中!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
79 | this.Shutdown();
80 | return;
81 | }
82 |
83 | // 额外的进程检查(双重保险)
84 | if (Program.IsAnotherInstanceRunning())
85 | {
86 | MessageBox.Show("检测到其他实例正在运行!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
87 | this.Shutdown();
88 | return;
89 | }
90 | }
91 | catch (AbandonedMutexException)
92 | {
93 | // 处理被遗弃的互斥锁(正常情况)
94 | mutexAcquired = true;
95 | }
96 | catch (Exception ex)
97 | {
98 | MessageBox.Show($"单实例检查失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
99 | this.Shutdown();
100 | return;
101 | }
102 | }
103 |
104 | try
105 | {
106 | // 确保应用程序数据目录存在
107 | string appDataPath = Program.GetAppDataPath();
108 | if (!Directory.Exists(appDataPath))
109 | {
110 | Directory.CreateDirectory(appDataPath);
111 | }
112 |
113 | base.OnStartup(e);
114 |
115 | // 创建主窗体并传递重启模式标志
116 | var mainWindow = new MainWindow(isRestartMode);
117 |
118 | // 显示主窗体以便能看到托盘图标
119 | mainWindow.Show();
120 |
121 | // 设置主窗体
122 | this.MainWindow = mainWindow;
123 | }
124 | catch (Exception ex)
125 | {
126 | MessageBox.Show($"启动应用程序时发生错误: {ex.Message}\n\n堆栈跟踪:\n{ex.StackTrace}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
127 | this.Shutdown();
128 | }
129 | }
130 |
131 | protected override void OnExit(ExitEventArgs e)
132 | {
133 | try
134 | {
135 | Program.ReleaseSingleInstanceMutex();
136 | }
137 | catch (Exception)
138 | {
139 | // 忽略释放异常
140 | }
141 |
142 | base.OnExit(e);
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/Line/VerticalLineForm.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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/Line/HorizontalLineForm.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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 屏幕参考线工具 / Screen Reference Line Tool
2 |
3 | [English](#english) | [中文](#中文)
4 |
5 | ---
6 |
7 | ## 中文
8 |
9 | ### 📖 简介
10 |
11 | 屏幕参考线工具是一款专业的屏幕辅助工具,帮助设计师、开发者和其他需要精确对齐的用户在屏幕上创建参考线。支持横线、竖线和包围框,具有丰富的自定义选项和智能穿透功能。
12 |
13 | ### ✨ 主要功能
14 |
15 | #### 🔹 瞬时横线
16 | - **热键触发**:默认 F5,可自定义
17 | - **自动消失**:显示后自动淡出
18 | - **多屏支持**:可在单屏或所有屏幕显示
19 | - **跟随鼠标**:在鼠标位置显示横线
20 |
21 | #### 🔹 持续竖线
22 | - **热键控制**:Ctrl+F1~F4 开启,Ctrl+Shift+F1~F4 关闭
23 | - **可拖拽**:非穿透模式下可拖拽调整位置
24 | - **多条支持**:最多同时显示4条竖线
25 |
26 | #### 🔹 持续横线
27 | - **热键控制**:Ctrl+1~4 开启,Ctrl+Shift+1~4 关闭
28 | - **可拖拽**:非穿透模式下可拖拽调整位置
29 | - **多屏模式**:支持单屏或全屏显示
30 |
31 | #### 🔹 包围框功能
32 | - **智能框选**:创建矩形参考框
33 | - **精确定位**:辅助元素对齐
34 |
35 | ### ⚙️ 自定义选项
36 |
37 | #### 🎨 外观设置
38 | - **线条粗细**:1-5像素可选
39 | - **线条颜色**:9种预设颜色 + 自定义颜色
40 | - **透明度**:25%、50%、75%、100% 可选
41 |
42 | #### ⏱️ 显示设置
43 | - **显示时长**:0.1-5秒可调(仅瞬时横线)
44 | - **鼠标穿透**:可开启/关闭
45 | - **显示模式**:单屏/全屏切换
46 |
47 | #### 🎯 高级功能
48 | - **持续置顶**:与其他程序抢夺置顶权
49 | - **置顶策略**:暴力定时器/智能监听
50 | - **监控程序**:自定义需要抢夺置顶的程序
51 | - **快捷键管理**:可全局禁用所有快捷键
52 |
53 | ### 🚀 快速开始
54 |
55 | 1. **下载并运行**:解压后直接运行 `Line.exe`
56 | 2. **系统托盘**:程序将在系统托盘显示图标
57 | 3. **右键菜单**:右键托盘图标打开设置菜单
58 | 4. **开始使用**:按 F5 显示瞬时横线,或使用其他快捷键
59 |
60 | ### ⌨️ 快捷键列表
61 |
62 | | 功能 | 快捷键 | 说明 |
63 | |------|--------|------|
64 | | 瞬时横线 | F5(默认) | 显示横线后自动消失 |
65 | | 竖线 1-4 | Ctrl+F1~F4 | 开启持续竖线 |
66 | | 关闭竖线 | Ctrl+Shift+F1~F4 | 关闭对应竖线 |
67 | | 横线 1-4 | Ctrl+1~4 | 开启持续横线 |
68 | | 关闭横线 | Ctrl+Shift+1~4 | 关闭对应横线 |
69 |
70 | ### 🔧 系统要求
71 |
72 | - **操作系统**:Windows 10 或更高版本
73 | - **框架**:.NET Framework 4.7.2 或更高版本
74 | - **权限**:管理员权限(用于全局热键注册)
75 |
76 | ### 📁 配置文件
77 |
78 | 配置文件自动保存在:
79 | ```
80 | %AppData%\ScreenLine\
81 | ├── config.json # 主程序配置
82 | ├── vertical_config.json # 竖线配置
83 | └── horizontal_config.json # 横线配置
84 | ```
85 |
86 | ### 🔄 更新日志
87 |
88 | #### v2.0.0
89 | - ✅ 修复鼠标穿透问题
90 | - ✅ 添加包围框功能
91 | - ✅ 增强置顶策略
92 | - ✅ 添加全局快捷键开关
93 | - ✅ 优化用户界面
94 |
95 | #### v1.0.0
96 | - ✅ 基础横线、竖线功能
97 | - ✅ 多屏支持
98 | - ✅ 自定义外观
99 | - ✅ 系统托盘集成
100 |
101 | ### 🐛 问题反馈
102 |
103 | 如果遇到问题或有功能建议,请通过以下方式反馈:
104 | - 提交 GitHub Issue
105 | - 发送邮件说明问题详情
106 |
107 | ---
108 |
109 | ## English
110 |
111 | ### 📖 Introduction
112 |
113 | Screen Reference Line Tool is a professional screen assistance utility that helps designers, developers, and other users who need precise alignment to create reference lines on screen. It supports horizontal lines, vertical lines, and bounding boxes with rich customization options and intelligent click-through functionality.
114 |
115 | ### ✨ Key Features
116 |
117 | #### 🔹 Temporary Horizontal Lines
118 | - **Hotkey Triggered**: Default F5, customizable
119 | - **Auto Fade**: Automatically disappears after display
120 | - **Multi-Screen Support**: Display on single screen or all screens
121 | - **Mouse Following**: Shows horizontal line at mouse position
122 |
123 | #### 🔹 Persistent Vertical Lines
124 | - **Hotkey Control**: Ctrl+F1~F4 to open, Ctrl+Shift+F1~F4 to close
125 | - **Draggable**: Can be dragged to adjust position in non-click-through mode
126 | - **Multi-Line Support**: Up to 4 vertical lines simultaneously
127 |
128 | #### 🔹 Persistent Horizontal Lines
129 | - **Hotkey Control**: Ctrl+1~4 to open, Ctrl+Shift+1~4 to close
130 | - **Draggable**: Can be dragged to adjust position in non-click-through mode
131 | - **Multi-Screen Mode**: Supports single screen or full screen display
132 |
133 | #### 🔹 Bounding Box Feature
134 | - **Smart Selection**: Create rectangular reference frames
135 | - **Precise Positioning**: Assist with element alignment
136 |
137 | ### ⚙️ Customization Options
138 |
139 | #### 🎨 Appearance Settings
140 | - **Line Thickness**: 1-5 pixels selectable
141 | - **Line Color**: 9 preset colors + custom colors
142 | - **Transparency**: 25%, 50%, 75%, 100% options
143 |
144 | #### ⏱️ Display Settings
145 | - **Display Duration**: 0.1-5 seconds adjustable (temporary lines only)
146 | - **Mouse Click-Through**: Can be enabled/disabled
147 | - **Display Mode**: Single screen/full screen toggle
148 |
149 | #### 🎯 Advanced Features
150 | - **Persistent Topmost**: Compete for topmost status with other programs
151 | - **Topmost Strategy**: Force timer/Smart monitoring
152 | - **Monitor Programs**: Customize programs to compete for topmost status
153 | - **Hotkey Management**: Can globally disable all hotkeys
154 |
155 | ### 🚀 Quick Start
156 |
157 | 1. **Download and Run**: Extract and run `Line.exe` directly
158 | 2. **System Tray**: Program will display icon in system tray
159 | 3. **Right-Click Menu**: Right-click tray icon to open settings menu
160 | 4. **Start Using**: Press F5 to show temporary horizontal line, or use other hotkeys
161 |
162 | ### ⌨️ Hotkey List
163 |
164 | | Function | Hotkey | Description |
165 | |----------|--------|-------------|
166 | | Temporary Line | F5 (default) | Show horizontal line with auto fade |
167 | | Vertical Line 1-4 | Ctrl+F1~F4 | Open persistent vertical lines |
168 | | Close Vertical | Ctrl+Shift+F1~F4 | Close corresponding vertical line |
169 | | Horizontal Line 1-4 | Ctrl+1~4 | Open persistent horizontal lines |
170 | | Close Horizontal | Ctrl+Shift+1~4 | Close corresponding horizontal line |
171 |
172 | ### 🔧 System Requirements
173 |
174 | - **Operating System**: Windows 10 or higher
175 | - **Framework**: .NET Framework 4.7.2 or higher
176 | - **Permissions**: Administrator privileges (for global hotkey registration)
177 |
178 | ### 📁 Configuration Files
179 |
180 | Configuration files are automatically saved to:
181 | ```
182 | %AppData%\ScreenLine\
183 | ├── config.json # Main program configuration
184 | ├── vertical_config.json # Vertical line configuration
185 | └── horizontal_config.json # Horizontal line configuration
186 | ```
187 |
188 | ### 🔄 Changelog
189 |
190 | #### v2.0.0
191 | - ✅ Fixed mouse click-through issues
192 | - ✅ Added bounding box functionality
193 | - ✅ Enhanced topmost strategies
194 | - ✅ Added global hotkey toggle
195 | - ✅ Improved user interface
196 |
197 | #### v1.0.0
198 | - ✅ Basic horizontal and vertical line features
199 | - ✅ Multi-screen support
200 | - ✅ Custom appearance
201 | - ✅ System tray integration
202 |
203 | ### 🐛 Feedback
204 |
205 | If you encounter issues or have feature suggestions, please provide feedback through:
206 | - Submit GitHub Issues
207 | - Send email with detailed problem description
208 |
209 | ---
210 |
211 | ### 📄 License
212 |
213 | This project is licensed under the MIT License - see the LICENSE file for details.
214 |
215 | ### 🤝 Contributing
216 |
217 | Contributions are welcome! Please feel free to submit a Pull Request.
218 |
219 | ### ⭐ Star History
220 |
221 | If this tool helps you, please consider giving it a star! ⭐
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/Line_wpf/MonitoredAppsWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Windows;
8 | using System.Windows.Input;
9 | using System.Windows.Interop;
10 |
11 | namespace Line_wpf
12 | {
13 | public partial class MonitoredAppsWindow : Window
14 | {
15 | public ObservableCollection MonitoredApps { get; private set; }
16 | public bool DialogResultOK { get; private set; }
17 |
18 | // Windows API for window detection and global mouse hooks
19 | [DllImport("user32.dll")]
20 | private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
21 |
22 | [DllImport("user32.dll")]
23 | private static extern IntPtr WindowFromPoint(POINT point);
24 |
25 | [DllImport("user32.dll")]
26 | private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
27 |
28 | [DllImport("user32.dll")]
29 | private static extern IntPtr SetCapture(IntPtr hWnd);
30 |
31 | [DllImport("user32.dll")]
32 | private static extern bool ReleaseCapture();
33 |
34 | [DllImport("user32.dll")]
35 | private static extern IntPtr GetCapture();
36 |
37 | [DllImport("user32.dll")]
38 | private static extern bool SetCursorPos(int x, int y);
39 |
40 | [DllImport("user32.dll")]
41 | private static extern bool GetCursorPos(out POINT lpPoint);
42 |
43 | [DllImport("user32.dll")]
44 | private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
45 |
46 | [DllImport("user32.dll")]
47 | private static extern bool UnhookWindowsHookEx(IntPtr hhk);
48 |
49 | [DllImport("user32.dll")]
50 | private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
51 |
52 | [DllImport("kernel32.dll")]
53 | private static extern IntPtr GetModuleHandle(string lpModuleName);
54 |
55 | private const int WH_MOUSE_LL = 14;
56 | private const int WM_LBUTTONDOWN = 0x0201;
57 | private const int WM_LBUTTONUP = 0x0202;
58 | private const int WM_MOUSEMOVE = 0x0200;
59 |
60 | private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
61 |
62 | [StructLayout(LayoutKind.Sequential)]
63 | private struct RECT
64 | {
65 | public int Left;
66 | public int Top;
67 | public int Right;
68 | public int Bottom;
69 | }
70 |
71 | [StructLayout(LayoutKind.Sequential)]
72 | private struct POINT
73 | {
74 | public int x;
75 | public int y;
76 | }
77 |
78 | [StructLayout(LayoutKind.Sequential)]
79 | private struct MSLLHOOKSTRUCT
80 | {
81 | public POINT pt;
82 | public uint mouseData;
83 | public uint flags;
84 | public uint time;
85 | public IntPtr dwExtraInfo;
86 | }
87 |
88 | // Window picker state
89 | private bool isPickingWindow = false;
90 | private bool isDragging = false;
91 | private System.Windows.Input.Cursor originalCursor;
92 | private IntPtr hookID = IntPtr.Zero;
93 | private LowLevelMouseProc hookProc;
94 |
95 | public MonitoredAppsWindow(List currentApps)
96 | {
97 | InitializeComponent();
98 |
99 | // 深拷贝当前应用程序列表
100 | MonitoredApps = new ObservableCollection();
101 | foreach (var app in currentApps)
102 | {
103 | MonitoredApps.Add(new MainWindow.MonitoredApp(app.Name, app.IsEnabled));
104 | }
105 |
106 | // 设置数据源
107 | dataGrid.ItemsSource = MonitoredApps;
108 |
109 | // 初始化鼠标钩子回调
110 | hookProc = HookCallback;
111 | }
112 |
113 | private void AddButton_Click(object sender, RoutedEventArgs e)
114 | {
115 | var inputDialog = new InputDialog("添加监控程序", "请输入要监控的程序窗口标题:");
116 | if (inputDialog.ShowDialog() == true)
117 | {
118 | string appName = inputDialog.InputText.Trim();
119 | if (!string.IsNullOrWhiteSpace(appName))
120 | {
121 | // 检查是否已存在
122 | if (!MonitoredApps.Any(a => a.Name.Equals(appName, StringComparison.OrdinalIgnoreCase)))
123 | {
124 | MonitoredApps.Add(new MainWindow.MonitoredApp(appName, true));
125 | }
126 | else
127 | {
128 | System.Windows.MessageBox.Show("该程序已在列表中!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
129 | }
130 | }
131 | }
132 | }
133 |
134 | private void DeleteButton_Click(object sender, RoutedEventArgs e)
135 | {
136 | if (dataGrid.SelectedItem is MainWindow.MonitoredApp selectedApp)
137 | {
138 | var result = System.Windows.MessageBox.Show($"确定要删除 \"{selectedApp.Name}\" 吗?",
139 | "确认删除", MessageBoxButton.YesNo, MessageBoxImage.Question);
140 |
141 | if (result == MessageBoxResult.Yes)
142 | {
143 | MonitoredApps.Remove(selectedApp);
144 | }
145 | }
146 | else
147 | {
148 | System.Windows.MessageBox.Show("请先选择要删除的程序!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
149 | }
150 | }
151 |
152 | private void WindowPickerButton_MouseDown(object sender, MouseButtonEventArgs e)
153 | {
154 | if (e.LeftButton == MouseButtonState.Pressed)
155 | {
156 | StartWindowPicking();
157 | }
158 | }
159 |
160 | private void WindowPickerButton_MouseUp(object sender, MouseButtonEventArgs e)
161 | {
162 | if (isPickingWindow && isDragging)
163 | {
164 | StopWindowPicking();
165 | }
166 | }
167 |
168 | private void StartWindowPicking()
169 | {
170 | isPickingWindow = true;
171 | isDragging = false;
172 | originalCursor = this.Cursor;
173 |
174 | // 设置全局鼠标钩子
175 | hookID = SetWindowsHookEx(WH_MOUSE_LL, hookProc, GetModuleHandle("user32"), 0);
176 |
177 | // 更新按钮状态
178 | windowPickerButton.Content = "松开获取";
179 | windowPickerButton.Background = System.Windows.Media.Brushes.LightCoral;
180 |
181 | // 设置十字光标
182 | this.Cursor = System.Windows.Input.Cursors.Cross;
183 |
184 | // 直接开始拖拽
185 | isDragging = true;
186 | }
187 |
188 | private void StopWindowPicking()
189 | {
190 | if (hookID != IntPtr.Zero)
191 | {
192 | UnhookWindowsHookEx(hookID);
193 | hookID = IntPtr.Zero;
194 | }
195 |
196 | isPickingWindow = false;
197 | isDragging = false;
198 |
199 | // 恢复按钮状态
200 | windowPickerButton.Content = "🎯 拖拽拾取";
201 | windowPickerButton.Background = System.Windows.SystemColors.ControlBrush;
202 |
203 | // 恢复光标
204 | this.Cursor = originalCursor;
205 |
206 | // 获取鼠标位置并检测窗口
207 | if (GetCursorPos(out POINT point))
208 | {
209 | IntPtr hwnd = WindowFromPoint(point);
210 | if (hwnd != IntPtr.Zero)
211 | {
212 | StringBuilder title = new StringBuilder(256);
213 | GetWindowText(hwnd, title, title.Capacity);
214 | string windowTitle = title.ToString();
215 |
216 | ProcessCapturedWindow(windowTitle);
217 | }
218 | }
219 | }
220 |
221 | private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
222 | {
223 | if (nCode >= 0 && isPickingWindow)
224 | {
225 | if (wParam == (IntPtr)WM_LBUTTONUP && isDragging)
226 | {
227 | // 在UI线程中停止窗口拾取
228 | this.Dispatcher.Invoke(() => {
229 | StopWindowPicking();
230 | });
231 | return (IntPtr)1; // 阻止消息传递
232 | }
233 | }
234 |
235 | return CallNextHookEx(hookID, nCode, wParam, lParam);
236 | }
237 |
238 | private void ProcessCapturedWindow(string title)
239 | {
240 | if (!string.IsNullOrWhiteSpace(title))
241 | {
242 | // 询问用户是否添加
243 | var result = System.Windows.MessageBox.Show($"检测到窗口标题:\n\n\"{title}\"\n\n是否添加到监控列表?",
244 | "确认添加", MessageBoxButton.YesNo, MessageBoxImage.Question);
245 |
246 | if (result == MessageBoxResult.Yes)
247 | {
248 | // 检查是否已存在
249 | if (!MonitoredApps.Any(a => a.Name.Equals(title, StringComparison.OrdinalIgnoreCase)))
250 | {
251 | MonitoredApps.Add(new MainWindow.MonitoredApp(title, true));
252 | System.Windows.MessageBox.Show("已成功添加到监控列表!", "添加成功",
253 | MessageBoxButton.OK, MessageBoxImage.Information);
254 | }
255 | else
256 | {
257 | System.Windows.MessageBox.Show("该程序已在列表中!", "提示",
258 | MessageBoxButton.OK, MessageBoxImage.Information);
259 | }
260 | }
261 | }
262 | else
263 | {
264 | System.Windows.MessageBox.Show("无法获取窗口标题,请重试。", "获取失败",
265 | MessageBoxButton.OK, MessageBoxImage.Warning);
266 | }
267 | }
268 |
269 | private void SaveButton_Click(object sender, RoutedEventArgs e)
270 | {
271 | DialogResultOK = true;
272 | this.DialogResult = true;
273 | this.Close();
274 | }
275 |
276 | private void CancelButton_Click(object sender, RoutedEventArgs e)
277 | {
278 | DialogResultOK = false;
279 | this.DialogResult = false;
280 | this.Close();
281 | }
282 |
283 | protected override void OnClosed(EventArgs e)
284 | {
285 | if (isPickingWindow)
286 | {
287 | StopWindowPicking();
288 | }
289 | base.OnClosed(e);
290 | }
291 | }
292 |
293 | // 简单的输入对话框
294 | public partial class InputDialog : Window
295 | {
296 | public string InputText { get; private set; } = "";
297 |
298 | public InputDialog(string title, string prompt)
299 | {
300 | InitializeInputDialog(title, prompt);
301 | }
302 |
303 | private void InitializeInputDialog(string title, string prompt)
304 | {
305 | this.Title = title;
306 | this.Width = 400;
307 | this.Height = 150;
308 | this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
309 | this.ResizeMode = ResizeMode.NoResize;
310 |
311 | var grid = new System.Windows.Controls.Grid();
312 | grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = new GridLength(40) });
313 | grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = new GridLength(40) });
314 | grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = new GridLength(40) });
315 |
316 | var label = new System.Windows.Controls.Label
317 | {
318 | Content = prompt,
319 | Margin = new Thickness(12, 10, 12, 0)
320 | };
321 | System.Windows.Controls.Grid.SetRow(label, 0);
322 |
323 | var textBox = new System.Windows.Controls.TextBox
324 | {
325 | Margin = new Thickness(12, 5, 12, 5),
326 | VerticalAlignment = VerticalAlignment.Center
327 | };
328 | System.Windows.Controls.Grid.SetRow(textBox, 1);
329 |
330 | var buttonPanel = new System.Windows.Controls.StackPanel
331 | {
332 | Orientation = System.Windows.Controls.Orientation.Horizontal,
333 | HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
334 | Margin = new Thickness(12, 5, 12, 5)
335 | };
336 |
337 | var okButton = new System.Windows.Controls.Button
338 | {
339 | Content = "确定",
340 | Width = 75,
341 | Height = 25,
342 | Margin = new Thickness(0, 0, 8, 0),
343 | IsDefault = true
344 | };
345 | okButton.Click += (s, e) => {
346 | InputText = textBox.Text;
347 | this.DialogResult = true;
348 | };
349 |
350 | var cancelButton = new System.Windows.Controls.Button
351 | {
352 | Content = "取消",
353 | Width = 75,
354 | Height = 25,
355 | IsCancel = true
356 | };
357 | cancelButton.Click += (s, e) => {
358 | this.DialogResult = false;
359 | };
360 |
361 | buttonPanel.Children.Add(okButton);
362 | buttonPanel.Children.Add(cancelButton);
363 | System.Windows.Controls.Grid.SetRow(buttonPanel, 2);
364 |
365 | grid.Children.Add(label);
366 | grid.Children.Add(textBox);
367 | grid.Children.Add(buttonPanel);
368 |
369 | this.Content = grid;
370 |
371 | // 设置焦点到文本框
372 | this.Loaded += (s, e) => textBox.Focus();
373 | }
374 | }
375 | }
--------------------------------------------------------------------------------
/Line/MonitoredAppsWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Windows.Forms;
8 |
9 | namespace Line
10 | {
11 | public partial class MonitoredAppsWindow : Form
12 | {
13 | private DataGridView dataGridView;
14 | private Button addButton;
15 | private Button deleteButton;
16 | private Button windowPickerButton;
17 | private Button saveButton;
18 | private Button cancelButton;
19 |
20 | public List MonitoredApps { get; private set; }
21 | public bool DialogResultOK { get; private set; }
22 |
23 | // Windows API for window detection and global mouse hooks
24 | [DllImport("user32.dll")]
25 | private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
26 |
27 | [DllImport("user32.dll")]
28 | private static extern IntPtr WindowFromPoint(Point point);
29 |
30 | [DllImport("user32.dll")]
31 | private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
32 |
33 | [DllImport("user32.dll")]
34 | private static extern IntPtr SetCapture(IntPtr hWnd);
35 |
36 | [DllImport("user32.dll")]
37 | private static extern bool ReleaseCapture();
38 |
39 | [DllImport("user32.dll")]
40 | private static extern IntPtr GetCapture();
41 |
42 | [DllImport("user32.dll")]
43 | private static extern bool SetCursorPos(int x, int y);
44 |
45 | [DllImport("user32.dll")]
46 | private static extern bool GetCursorPos(out Point lpPoint);
47 |
48 | [DllImport("user32.dll")]
49 | private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
50 |
51 | [DllImport("user32.dll")]
52 | private static extern bool UnhookWindowsHookEx(IntPtr hhk);
53 |
54 | [DllImport("user32.dll")]
55 | private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
56 |
57 | [DllImport("kernel32.dll")]
58 | private static extern IntPtr GetModuleHandle(string lpModuleName);
59 |
60 | private const int WH_MOUSE_LL = 14;
61 | private const int WM_LBUTTONDOWN = 0x0201;
62 | private const int WM_LBUTTONUP = 0x0202;
63 | private const int WM_MOUSEMOVE = 0x0200;
64 |
65 | private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
66 |
67 | [StructLayout(LayoutKind.Sequential)]
68 | private struct RECT
69 | {
70 | public int Left;
71 | public int Top;
72 | public int Right;
73 | public int Bottom;
74 | }
75 |
76 | [StructLayout(LayoutKind.Sequential)]
77 | private struct POINT
78 | {
79 | public int x;
80 | public int y;
81 | }
82 |
83 | [StructLayout(LayoutKind.Sequential)]
84 | private struct MSLLHOOKSTRUCT
85 | {
86 | public POINT pt;
87 | public uint mouseData;
88 | public uint flags;
89 | public uint time;
90 | public IntPtr dwExtraInfo;
91 | }
92 |
93 | // Window picker state
94 | private bool isPickingWindow = false;
95 | private bool isDragging = false;
96 | private Cursor originalCursor;
97 | private IntPtr hookID = IntPtr.Zero;
98 | private LowLevelMouseProc hookProc;
99 |
100 | public MonitoredAppsWindow(List currentApps)
101 | {
102 | InitializeComponent();
103 |
104 | // 深拷贝当前应用程序列表
105 | MonitoredApps = new List();
106 | foreach (var app in currentApps)
107 | {
108 | MonitoredApps.Add(new LineForm.MonitoredApp(app.Name, app.IsEnabled));
109 | }
110 |
111 | SetupDataGridView();
112 | RefreshDataGridView();
113 |
114 | // 初始化鼠标钩子回调
115 | hookProc = HookCallback;
116 | }
117 |
118 | private void InitializeComponent()
119 | {
120 | this.Text = "管理监控程序";
121 | this.Size = new Size(600, 450);
122 | this.StartPosition = FormStartPosition.CenterScreen;
123 | this.MaximizeBox = false;
124 | this.MinimizeBox = false;
125 | this.FormBorderStyle = FormBorderStyle.FixedDialog;
126 |
127 | // 创建DataGridView
128 | dataGridView = new DataGridView
129 | {
130 | Location = new Point(12, 12),
131 | Size = new Size(560, 320),
132 | AllowUserToAddRows = false,
133 | AllowUserToDeleteRows = false,
134 | AllowUserToResizeRows = false,
135 | RowHeadersVisible = false,
136 | SelectionMode = DataGridViewSelectionMode.FullRowSelect,
137 | MultiSelect = false,
138 | ReadOnly = false
139 | };
140 |
141 | // 创建按钮
142 | addButton = new Button
143 | {
144 | Text = "添加程序",
145 | Location = new Point(12, 340),
146 | Size = new Size(80, 30)
147 | };
148 | addButton.Click += AddButton_Click;
149 |
150 | deleteButton = new Button
151 | {
152 | Text = "删除选中",
153 | Location = new Point(100, 340),
154 | Size = new Size(80, 30)
155 | };
156 | deleteButton.Click += DeleteButton_Click;
157 |
158 | windowPickerButton = new Button
159 | {
160 | Text = "🎯 拖拽拾取",
161 | Location = new Point(188, 340),
162 | Size = new Size(90, 30),
163 | };
164 | windowPickerButton.MouseDown += WindowPickerButton_MouseDown;
165 |
166 | saveButton = new Button
167 | {
168 | Text = "保存",
169 | Location = new Point(412, 380),
170 | Size = new Size(75, 30)
171 | };
172 | saveButton.Click += SaveButton_Click;
173 |
174 | cancelButton = new Button
175 | {
176 | Text = "取消",
177 | Location = new Point(497, 380),
178 | Size = new Size(75, 30)
179 | };
180 | cancelButton.Click += CancelButton_Click;
181 |
182 | // 添加控件到窗体
183 | this.Controls.Add(dataGridView);
184 | this.Controls.Add(addButton);
185 | this.Controls.Add(deleteButton);
186 | this.Controls.Add(windowPickerButton);
187 | this.Controls.Add(saveButton);
188 | this.Controls.Add(cancelButton);
189 | }
190 |
191 | private void SetupDataGridView()
192 | {
193 | // 添加列
194 | var nameColumn = new DataGridViewTextBoxColumn
195 | {
196 | Name = "Name",
197 | HeaderText = "程序标题",
198 | DataPropertyName = "Name",
199 | Width = 460,
200 | ReadOnly = true
201 | };
202 |
203 | var enabledColumn = new DataGridViewCheckBoxColumn
204 | {
205 | Name = "IsEnabled",
206 | HeaderText = "启用",
207 | DataPropertyName = "IsEnabled",
208 | Width = 80
209 | };
210 |
211 | dataGridView.Columns.Add(nameColumn);
212 | dataGridView.Columns.Add(enabledColumn);
213 |
214 | // 禁用自动调整列宽
215 | dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
216 |
217 | // 添加单元格值变化事件处理
218 | dataGridView.CellValueChanged += DataGridView_CellValueChanged;
219 | dataGridView.CurrentCellDirtyStateChanged += DataGridView_CurrentCellDirtyStateChanged;
220 | }
221 |
222 | private void DataGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e)
223 | {
224 | // 当复选框状态改变时立即提交更改
225 | if (dataGridView.IsCurrentCellDirty)
226 | {
227 | dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
228 | }
229 | }
230 |
231 | private void DataGridView_CellValueChanged(object sender, DataGridViewCellEventArgs e)
232 | {
233 | // 当单元格值发生变化时,同步到我们的数据列表
234 | if (e.RowIndex >= 0 && e.RowIndex < MonitoredApps.Count && e.ColumnIndex == 1) // IsEnabled 列
235 | {
236 | var cell = dataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex];
237 | if (cell.Value is bool enabled)
238 | {
239 | MonitoredApps[e.RowIndex].IsEnabled = enabled;
240 | }
241 | }
242 | }
243 |
244 | private void RefreshDataGridView()
245 | {
246 | dataGridView.DataSource = null;
247 | dataGridView.DataSource = MonitoredApps;
248 |
249 | // 重新设置列宽,确保不会被重置
250 | if (dataGridView.Columns.Count > 0)
251 | {
252 | dataGridView.Columns[0].Width = 460; // 程序标题列
253 | if (dataGridView.Columns.Count > 1)
254 | {
255 | dataGridView.Columns[1].Width = 80; // 启用列
256 | }
257 | }
258 |
259 | // 确保禁用自动调整列宽
260 | dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
261 | }
262 |
263 | private void AddButton_Click(object sender, EventArgs e)
264 | {
265 | using (var inputForm = new InputForm("添加监控程序", "请输入要监控的程序窗口标题:"))
266 | {
267 | if (inputForm.ShowDialog() == DialogResult.OK)
268 | {
269 | string appName = inputForm.InputText.Trim();
270 | if (!string.IsNullOrWhiteSpace(appName))
271 | {
272 | // 检查是否已存在
273 | if (!MonitoredApps.Any(a => a.Name.Equals(appName, StringComparison.OrdinalIgnoreCase)))
274 | {
275 | MonitoredApps.Add(new LineForm.MonitoredApp(appName, true));
276 | RefreshDataGridView();
277 | }
278 | else
279 | {
280 | MessageBox.Show("该程序已在列表中!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
281 | }
282 | }
283 | }
284 | }
285 | }
286 |
287 | private void DeleteButton_Click(object sender, EventArgs e)
288 | {
289 | if (dataGridView.SelectedRows.Count > 0)
290 | {
291 | int selectedIndex = dataGridView.SelectedRows[0].Index;
292 | if (selectedIndex >= 0 && selectedIndex < MonitoredApps.Count)
293 | {
294 | var selectedApp = MonitoredApps[selectedIndex];
295 | var result = MessageBox.Show($"确定要删除 \"{selectedApp.Name}\" 吗?",
296 | "确认删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
297 |
298 | if (result == DialogResult.Yes)
299 | {
300 | MonitoredApps.RemoveAt(selectedIndex);
301 | RefreshDataGridView();
302 | }
303 | }
304 | }
305 | else
306 | {
307 | MessageBox.Show("请先选择要删除的程序!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
308 | }
309 | }
310 |
311 | private void WindowPickerButton_MouseDown(object sender, MouseEventArgs e)
312 | {
313 | if (e.Button == MouseButtons.Left)
314 | {
315 | StartWindowPicking();
316 | }
317 | }
318 |
319 | private void StartWindowPicking()
320 | {
321 | isPickingWindow = true;
322 | isDragging = false;
323 | originalCursor = this.Cursor;
324 |
325 | // 设置全局鼠标钩子
326 | hookID = SetWindowsHookEx(WH_MOUSE_LL, hookProc, GetModuleHandle("user32"), 0);
327 |
328 | // 更新按钮状态
329 | windowPickerButton.Text = "松开获取";
330 | windowPickerButton.BackColor = Color.LightCoral;
331 |
332 | // 设置十字光标
333 | Cursor.Current = Cursors.Cross;
334 |
335 | // 直接开始拖拽,无需弹窗提示
336 | isDragging = true;
337 | }
338 |
339 | private void StopWindowPicking()
340 | {
341 | isPickingWindow = false;
342 | isDragging = false;
343 |
344 | // 移除全局鼠标钩子
345 | if (hookID != IntPtr.Zero)
346 | {
347 | UnhookWindowsHookEx(hookID);
348 | hookID = IntPtr.Zero;
349 | }
350 |
351 | // 恢复按钮状态
352 | windowPickerButton.Text = "🎯 拖拽拾取";
353 | windowPickerButton.BackColor = SystemColors.Control;
354 |
355 | // 恢复光标
356 | Cursor.Current = Cursors.Default;
357 | }
358 |
359 | private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
360 | {
361 | if (nCode >= 0 && isPickingWindow)
362 | {
363 | int msgType = wParam.ToInt32();
364 |
365 | if (msgType == WM_LBUTTONDOWN)
366 | {
367 | // 开始拖拽
368 | isDragging = true;
369 | Cursor.Current = Cursors.Cross;
370 | }
371 | else if (msgType == WM_LBUTTONUP && isDragging)
372 | {
373 | // 结束拖拽,获取窗口信息
374 | isDragging = false;
375 |
376 | MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
377 | Point mousePoint = new Point(hookStruct.pt.x, hookStruct.pt.y);
378 |
379 | // 获取鼠标位置的窗口
380 | IntPtr targetWindow = WindowFromPoint(mousePoint);
381 |
382 | if (targetWindow != IntPtr.Zero && targetWindow != this.Handle)
383 | {
384 | // 获取窗口标题
385 | StringBuilder windowTitle = new StringBuilder(256);
386 | GetWindowText(targetWindow, windowTitle, windowTitle.Capacity);
387 | string title = windowTitle.ToString();
388 |
389 | // 停止拾取模式
390 | this.Invoke(new Action(() => {
391 | StopWindowPicking();
392 | ProcessCapturedWindow(title);
393 | }));
394 | }
395 | else
396 | {
397 | this.Invoke(new Action(() => {
398 | StopWindowPicking();
399 | }));
400 | }
401 | }
402 | else if (msgType == WM_MOUSEMOVE && isDragging)
403 | {
404 | // 在拖拽过程中保持十字光标
405 | Cursor.Current = Cursors.Cross;
406 | }
407 | }
408 |
409 | return CallNextHookEx(hookID, nCode, wParam, lParam);
410 | }
411 |
412 | private void ProcessCapturedWindow(string title)
413 | {
414 | if (!string.IsNullOrWhiteSpace(title))
415 | {
416 | // 询问用户是否添加
417 | var result = MessageBox.Show($"检测到窗口标题:\n\n\"{title}\"\n\n是否添加到监控列表?",
418 | "确认添加", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
419 |
420 | if (result == DialogResult.Yes)
421 | {
422 | // 检查是否已存在
423 | if (!MonitoredApps.Any(a => a.Name.Equals(title, StringComparison.OrdinalIgnoreCase)))
424 | {
425 | MonitoredApps.Add(new LineForm.MonitoredApp(title, true));
426 | RefreshDataGridView();
427 | MessageBox.Show("已成功添加到监控列表!", "添加成功",
428 | MessageBoxButtons.OK, MessageBoxIcon.Information);
429 | }
430 | else
431 | {
432 | MessageBox.Show("该程序已在列表中!", "提示",
433 | MessageBoxButtons.OK, MessageBoxIcon.Information);
434 | }
435 | }
436 | }
437 | else
438 | {
439 | MessageBox.Show("无法获取窗口标题,请重试。", "获取失败",
440 | MessageBoxButtons.OK, MessageBoxIcon.Warning);
441 | }
442 | }
443 |
444 | private void SaveButton_Click(object sender, EventArgs e)
445 | {
446 | // 确保所有编辑都被提交
447 | dataGridView.EndEdit();
448 |
449 | // 手动同步所有数据,确保没有遗漏
450 | for (int i = 0; i < dataGridView.Rows.Count && i < MonitoredApps.Count; i++)
451 | {
452 | var enabledCell = dataGridView.Rows[i].Cells["IsEnabled"];
453 | if (enabledCell.Value is bool enabled)
454 | {
455 | MonitoredApps[i].IsEnabled = enabled;
456 | }
457 | }
458 |
459 | DialogResultOK = true;
460 | this.DialogResult = DialogResult.OK;
461 | this.Close();
462 | }
463 |
464 | private void CancelButton_Click(object sender, EventArgs e)
465 | {
466 | DialogResultOK = false;
467 | this.Close();
468 | }
469 |
470 | protected override void OnFormClosing(FormClosingEventArgs e)
471 | {
472 | if (isPickingWindow)
473 | {
474 | StopWindowPicking();
475 | }
476 | base.OnFormClosing(e);
477 | }
478 | }
479 |
480 | // 简单的输入对话框
481 | public partial class InputForm : Form
482 | {
483 | private Label label;
484 | private TextBox textBox;
485 | private Button okButton;
486 | private Button cancelButton;
487 |
488 | public string InputText { get; private set; } = "";
489 |
490 | public InputForm(string title, string prompt)
491 | {
492 | InitializeComponent(title, prompt);
493 | }
494 |
495 | private void InitializeComponent(string title, string prompt)
496 | {
497 | this.Text = title;
498 | this.Size = new Size(400, 150);
499 | this.StartPosition = FormStartPosition.CenterParent;
500 | this.FormBorderStyle = FormBorderStyle.FixedDialog;
501 | this.MaximizeBox = false;
502 | this.MinimizeBox = false;
503 |
504 | label = new Label
505 | {
506 | Text = prompt,
507 | Location = new Point(12, 15),
508 | Size = new Size(360, 20)
509 | };
510 |
511 | textBox = new TextBox
512 | {
513 | Location = new Point(12, 40),
514 | Size = new Size(360, 25)
515 | };
516 |
517 | okButton = new Button
518 | {
519 | Text = "确定",
520 | Location = new Point(217, 75),
521 | Size = new Size(75, 25),
522 | DialogResult = DialogResult.OK
523 | };
524 | okButton.Click += (s, e) => { InputText = textBox.Text; };
525 |
526 | cancelButton = new Button
527 | {
528 | Text = "取消",
529 | Location = new Point(297, 75),
530 | Size = new Size(75, 25),
531 | DialogResult = DialogResult.Cancel
532 | };
533 |
534 | this.Controls.Add(label);
535 | this.Controls.Add(textBox);
536 | this.Controls.Add(okButton);
537 | this.Controls.Add(cancelButton);
538 |
539 | this.AcceptButton = okButton;
540 | this.CancelButton = cancelButton;
541 |
542 | // 设置焦点到文本框
543 | this.Load += (s, e) => textBox.Focus();
544 | }
545 | }
546 | }
--------------------------------------------------------------------------------
/Line/VerticalLineForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using System.Windows.Forms;
4 | using System.Runtime.InteropServices;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Text.Json;
8 | using System.Reflection;
9 |
10 | namespace Line
11 | {
12 | // 可拖拽的竖线类
13 | public class DraggableVerticalLine : Form
14 | {
15 | private bool isDragging = false;
16 | private Point lastCursor;
17 | private bool mouseClickThrough;
18 |
19 | // Windows API for mouse click-through
20 | private const int WS_EX_TRANSPARENT = 0x20;
21 | private const int WS_EX_LAYERED = 0x80000;
22 | private const int WS_EX_NOACTIVATE = 0x08000000;
23 | private const int GWL_EXSTYLE = (-20);
24 |
25 | // 新增:窗口消息常量
26 | private const int WM_SETCURSOR = 0x0020;
27 | private const int WM_MOUSEACTIVATE = 0x0021;
28 | private const int MA_NOACTIVATE = 3;
29 |
30 | [DllImport("user32.dll")]
31 | private static extern int GetWindowLong(IntPtr hwnd, int index);
32 |
33 | [DllImport("user32.dll")]
34 | private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
35 |
36 | public DraggableVerticalLine(int width, int height, Color color, double opacity, bool clickThrough)
37 | {
38 | this.FormBorderStyle = FormBorderStyle.None;
39 | this.ShowInTaskbar = false;
40 | this.TopMost = true;
41 | this.BackColor = color;
42 | this.TransparencyKey = Color.Black;
43 | this.Opacity = opacity;
44 | this.Width = width;
45 | this.Height = height;
46 | this.AutoScaleMode = AutoScaleMode.None;
47 | this.StartPosition = FormStartPosition.Manual;
48 |
49 | mouseClickThrough = clickThrough;
50 |
51 | // 如果不是鼠标穿透模式,添加拖拽事件和设置光标
52 | if (!mouseClickThrough)
53 | {
54 | this.MouseDown += OnMouseDown;
55 | this.MouseMove += OnMouseMove;
56 | this.MouseUp += OnMouseUp;
57 | this.Cursor = Cursors.SizeWE; // 设置为水平调整大小光标
58 | }
59 | }
60 |
61 | // 重写CreateParams,在创建窗口时就设置所有必要的扩展样式
62 | protected override CreateParams CreateParams
63 | {
64 | get
65 | {
66 | var cp = base.CreateParams;
67 | // 分层 + 点透 + 不激活
68 | cp.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
69 | return cp;
70 | }
71 | }
72 |
73 | protected override void OnHandleCreated(EventArgs e)
74 | {
75 | base.OnHandleCreated(e);
76 | // 强制保证都加上去
77 | int ex = GetWindowLong(this.Handle, GWL_EXSTYLE);
78 | ex |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
79 | SetWindowLong(this.Handle, GWL_EXSTYLE, ex);
80 | }
81 |
82 | public void SetClickThrough(bool enable)
83 | {
84 | mouseClickThrough = enable;
85 | if (this.Handle != IntPtr.Zero)
86 | {
87 | if (enable)
88 | {
89 | // 启用穿透模式:移除拖拽事件,不设置光标
90 | this.MouseDown -= OnMouseDown;
91 | this.MouseMove -= OnMouseMove;
92 | this.MouseUp -= OnMouseUp;
93 | // 不再设置 this.Cursor,让系统决定光标形状
94 | }
95 | else
96 | {
97 | // 禁用穿透模式:移除透明样式,添加拖拽事件
98 | int exStyle = GetWindowLong(this.Handle, GWL_EXSTYLE);
99 | SetWindowLong(this.Handle, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
100 | this.Cursor = Cursors.SizeWE;
101 | // 添加拖拽事件
102 | this.MouseDown -= OnMouseDown; // 先移除避免重复
103 | this.MouseMove -= OnMouseMove;
104 | this.MouseUp -= OnMouseUp;
105 | this.MouseDown += OnMouseDown;
106 | this.MouseMove += OnMouseMove;
107 | this.MouseUp += OnMouseUp;
108 | }
109 | }
110 | }
111 |
112 | // 拦截窗口消息,解决光标和激活问题
113 | protected override void WndProc(ref Message m)
114 | {
115 | if (mouseClickThrough)
116 | {
117 | if (m.Msg == WM_MOUSEACTIVATE)
118 | {
119 | // 不激活自己,直接交给下面窗口
120 | m.Result = new IntPtr(MA_NOACTIVATE);
121 | return;
122 | }
123 | if (m.Msg == WM_SETCURSOR)
124 | {
125 | // 不处理,让系统去给下面窗口设光标
126 | return;
127 | }
128 | }
129 | base.WndProc(ref m);
130 | }
131 |
132 | private void OnMouseDown(object sender, MouseEventArgs e)
133 | {
134 | if (e.Button == MouseButtons.Left)
135 | {
136 | isDragging = true;
137 | lastCursor = Cursor.Position;
138 | }
139 | }
140 |
141 | private void OnMouseMove(object sender, MouseEventArgs e)
142 | {
143 | if (isDragging)
144 | {
145 | Point currentCursor = Cursor.Position;
146 | int deltaX = currentCursor.X - lastCursor.X;
147 |
148 | this.Location = new Point(this.Location.X + deltaX, this.Location.Y);
149 | lastCursor = currentCursor;
150 | }
151 | }
152 |
153 | private void OnMouseUp(object sender, MouseEventArgs e)
154 | {
155 | if (e.Button == MouseButtons.Left)
156 | {
157 | isDragging = false;
158 | }
159 | }
160 | }
161 |
162 | public class VerticalLineForm : Form
163 | {
164 | private const int WM_HOTKEY = 0x0312;
165 | private Dictionary verticalLines = new Dictionary();
166 | private Dictionary lineStates = new Dictionary();
167 | private NotifyIcon trayIcon;
168 |
169 | // 线条默认宽度为1像素
170 | private int lineWidth = 1;
171 |
172 | // 线条颜色,默认为蓝色
173 | private Color lineColor = Color.Blue;
174 |
175 | // 线条透明度,默认为100%
176 | private int lineOpacity = 100;
177 |
178 | // 鼠标穿透设置
179 | private bool mouseClickThrough = true;
180 |
181 | // 热键ID基础值(1-4用于开启,5-8用于关闭)
182 | private const int BASE_HOTKEY_ID_ON = 100;
183 | private const int BASE_HOTKEY_ID_OFF = 200;
184 |
185 | // 热键绑定状态
186 | private bool[] hotkeyEnabled = new bool[] { true, false, false, false }; // 默认只启用第一组
187 |
188 | // 用于处理初始显示的标志
189 | private bool isFirstShow = true;
190 |
191 | // 修改配置文件路径
192 | private readonly string configPath = Path.Combine(
193 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
194 | "ScreenLine",
195 | "vertical_config.json"
196 | );
197 |
198 | // 配置类
199 | private class Config
200 | {
201 | public bool[] HotkeyEnabled { get; set; }
202 | public int LineWidth { get; set; }
203 | public string LineColor { get; set; }
204 | public int LineOpacity { get; set; }
205 | public bool MouseClickThrough { get; set; }
206 | }
207 |
208 | [DllImport("user32.dll")]
209 | private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
210 |
211 | [DllImport("user32.dll")]
212 | private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
213 |
214 | // 修饰键常量
215 | private const int MOD_CONTROL = 0x0002;
216 | private const int MOD_SHIFT = 0x0004;
217 |
218 | // 增强的置顶Windows API
219 | [DllImport("user32.dll")]
220 | private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
221 |
222 | [DllImport("user32.dll")]
223 | private static extern bool BringWindowToTop(IntPtr hWnd);
224 |
225 | // 置顶相关常量
226 | private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
227 | private const uint SWP_NOMOVE = 0x0002;
228 | private const uint SWP_NOSIZE = 0x0001;
229 | private const uint SWP_SHOWWINDOW = 0x0040;
230 |
231 | public VerticalLineForm(NotifyIcon existingTrayIcon)
232 | {
233 | this.AutoScaleMode = AutoScaleMode.None;
234 | this.trayIcon = existingTrayIcon;
235 |
236 | // 加载配置
237 | LoadConfig();
238 |
239 | InitializeComponent();
240 | InitializeHotkeys();
241 | AddVerticalLineMenuItems();
242 |
243 | // 设置初始鼠标穿透状态
244 | SetClickThrough(mouseClickThrough);
245 |
246 | // 显示一次初始竖线,然后立即隐藏
247 | ShowInitialLine();
248 | }
249 |
250 | private void ShowInitialLine()
251 | {
252 | if (isFirstShow)
253 | {
254 | // 创建并显示一个临时的竖线
255 | Form tempLine = new Form
256 | {
257 | FormBorderStyle = FormBorderStyle.None,
258 | ShowInTaskbar = false,
259 | TopMost = true,
260 | BackColor = lineColor,
261 | TransparencyKey = Color.Black,
262 | Opacity = lineOpacity / 100.0,
263 | Width = lineWidth,
264 | Height = Screen.PrimaryScreen.Bounds.Height,
265 |
266 | // ③ 临时线也要关掉缩放
267 | AutoScaleMode = AutoScaleMode.None,
268 | StartPosition = FormStartPosition.Manual,
269 | Location = new Point(
270 | Screen.PrimaryScreen.Bounds.Width / 2,
271 | Screen.PrimaryScreen.Bounds.Y)
272 | };
273 |
274 | // 放在屏幕中央
275 | tempLine.Location = new Point(
276 | Screen.PrimaryScreen.Bounds.Width / 2,
277 | Screen.PrimaryScreen.Bounds.Y
278 | );
279 |
280 | tempLine.Show();
281 | tempLine.Close();
282 | isFirstShow = false;
283 | }
284 |
285 | // 隐藏主窗体
286 | this.Opacity = 0;
287 | }
288 |
289 | private void InitializeComponent()
290 | {
291 | this.FormBorderStyle = FormBorderStyle.None;
292 | this.ShowInTaskbar = false;
293 | this.TopMost = true;
294 | this.BackColor = lineColor;
295 | this.TransparencyKey = Color.Black;
296 | this.AutoScaleMode = AutoScaleMode.None;
297 | this.Width = lineWidth;
298 | this.Height = Screen.PrimaryScreen.Bounds.Height;
299 | // 不在这里设置 Opacity,而是在 ShowInitialLine 中处理
300 | }
301 |
302 | private void InitializeHotkeys()
303 | {
304 | // 根据配置注册所有启用的热键
305 | for (int i = 0; i < hotkeyEnabled.Length; i++)
306 | {
307 | if (hotkeyEnabled[i])
308 | {
309 | RegisterHotkeyPair(i);
310 | }
311 | }
312 | }
313 |
314 | private void RegisterHotkeyPair(int index)
315 | {
316 | try
317 | {
318 | if (index >= 0 && index < 4)
319 | {
320 | Keys key = Keys.F1 + index;
321 | // 注册 Ctrl+F1-F4
322 | bool onSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index, MOD_CONTROL, (int)key);
323 | // 注册 Ctrl+Shift+F1-F4
324 | bool offSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index, MOD_CONTROL | MOD_SHIFT, (int)key);
325 |
326 | // 如果注册失败,更新状态并保存配置
327 | if (!onSuccess || !offSuccess)
328 | {
329 | hotkeyEnabled[index] = false;
330 | UpdateHotkeyMenuCheckedState();
331 | SaveConfig();
332 | MessageBox.Show($"热键 Ctrl+F{index + 1} 注册失败,可能已被其他程序占用。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
333 | }
334 | }
335 | }
336 | catch (Exception ex)
337 | {
338 | hotkeyEnabled[index] = false;
339 | UpdateHotkeyMenuCheckedState();
340 | SaveConfig();
341 | MessageBox.Show($"注册热键时发生错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
342 | }
343 | }
344 |
345 | private void UnregisterHotkeyPair(int index)
346 | {
347 | if (index >= 0 && index < 4)
348 | {
349 | UnregisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index);
350 | UnregisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index);
351 | }
352 | }
353 |
354 | private void SetClickThrough(bool enable)
355 | {
356 | mouseClickThrough = enable;
357 |
358 | // 更新所有现有的竖线
359 | foreach (var line in verticalLines.Values)
360 | {
361 | line.SetClickThrough(enable);
362 | }
363 |
364 | SaveConfig();
365 | }
366 |
367 | private void AddVerticalLineMenuItems()
368 | {
369 | if (trayIcon?.ContextMenuStrip == null) return;
370 |
371 | ToolStripMenuItem verticalLineMenu = new ToolStripMenuItem("持续竖线");
372 |
373 | // 热键绑定菜单
374 | ToolStripMenuItem hotkeyBindingMenu = new ToolStripMenuItem("热键绑定");
375 | for (int i = 0; i < 4; i++)
376 | {
377 | int index = i;
378 | var item = new ToolStripMenuItem($"Ctrl+F{i + 1}/Ctrl+Shift+F{i + 1}", null, (s, e) => {
379 | ToggleHotkeyBinding(index);
380 | });
381 | item.Checked = hotkeyEnabled[i];
382 | hotkeyBindingMenu.DropDownItems.Add(item);
383 | }
384 |
385 | // 鼠标穿透选项
386 | var mousePenetrationItem = new ToolStripMenuItem("鼠标穿透", null, (s, e) => {
387 | SetClickThrough(!mouseClickThrough);
388 | if (s is ToolStripMenuItem menuItem)
389 | {
390 | menuItem.Checked = mouseClickThrough;
391 | }
392 | });
393 | mousePenetrationItem.Checked = mouseClickThrough;
394 |
395 | // 线条粗细菜单
396 | ToolStripMenuItem lineThicknessItem = new ToolStripMenuItem("竖线粗细");
397 | AddThicknessMenuItem(lineThicknessItem, "细线 (1像素)", 1);
398 | AddThicknessMenuItem(lineThicknessItem, "中等 (2像素)", 2);
399 | AddThicknessMenuItem(lineThicknessItem, "粗线 (3像素)", 3);
400 | AddThicknessMenuItem(lineThicknessItem, "很粗 (5像素)", 5);
401 |
402 | // 线条颜色菜单
403 | ToolStripMenuItem lineColorItem = new ToolStripMenuItem("竖线颜色");
404 | AddColorMenuItem(lineColorItem, "红色", Color.Red);
405 | AddColorMenuItem(lineColorItem, "绿色", Color.Green);
406 | AddColorMenuItem(lineColorItem, "蓝色", Color.Blue);
407 | AddColorMenuItem(lineColorItem, "黄色", Color.Yellow);
408 | AddColorMenuItem(lineColorItem, "橙色", Color.Orange);
409 | AddColorMenuItem(lineColorItem, "紫色", Color.Purple);
410 | AddColorMenuItem(lineColorItem, "青色", Color.Cyan);
411 | AddColorMenuItem(lineColorItem, "黑色", Color.FromArgb(1, 1, 1));
412 | AddColorMenuItem(lineColorItem, "白色", Color.White);
413 |
414 | // 透明度菜单
415 | ToolStripMenuItem transparencyItem = new ToolStripMenuItem("竖线透明度");
416 | AddTransparencyMenuItem(transparencyItem, "100% (不透明)", 100);
417 | AddTransparencyMenuItem(transparencyItem, "75%", 75);
418 | AddTransparencyMenuItem(transparencyItem, "50%", 50);
419 | AddTransparencyMenuItem(transparencyItem, "25%", 25);
420 |
421 | // 添加所有子菜单
422 | verticalLineMenu.DropDownItems.Add(hotkeyBindingMenu);
423 | verticalLineMenu.DropDownItems.Add(mousePenetrationItem);
424 | verticalLineMenu.DropDownItems.Add(lineThicknessItem);
425 | verticalLineMenu.DropDownItems.Add(lineColorItem);
426 | verticalLineMenu.DropDownItems.Add(transparencyItem);
427 |
428 | // 在分隔符之前插入竖线菜单
429 | int separatorIndex = -1;
430 | for (int i = 0; i < trayIcon.ContextMenuStrip.Items.Count; i++)
431 | {
432 | if (trayIcon.ContextMenuStrip.Items[i] is ToolStripSeparator)
433 | {
434 | separatorIndex = i;
435 | break;
436 | }
437 | }
438 |
439 | if (separatorIndex != -1)
440 | {
441 | trayIcon.ContextMenuStrip.Items.Insert(separatorIndex, verticalLineMenu);
442 | }
443 | else
444 | {
445 | trayIcon.ContextMenuStrip.Items.Add(verticalLineMenu);
446 | }
447 | }
448 |
449 | private void AddThicknessMenuItem(ToolStripMenuItem parent, string text, int thickness)
450 | {
451 | var item = new ToolStripMenuItem(text, null, (s, e) => {
452 | ChangeLineThickness(thickness);
453 | });
454 | item.Checked = (thickness == lineWidth);
455 | parent.DropDownItems.Add(item);
456 | }
457 |
458 | private void AddColorMenuItem(ToolStripMenuItem parent, string name, Color color)
459 | {
460 | Bitmap colorPreview = new Bitmap(16, 16);
461 | using (Graphics g = Graphics.FromImage(colorPreview))
462 | {
463 | g.FillRectangle(new SolidBrush(color), 0, 0, 16, 16);
464 | g.DrawRectangle(Pens.Gray, 0, 0, 15, 15);
465 | }
466 |
467 | var item = new ToolStripMenuItem(name, colorPreview, (s, e) => {
468 | ChangeLineColor(color);
469 | });
470 | item.Checked = color.Equals(lineColor);
471 | parent.DropDownItems.Add(item);
472 | }
473 |
474 | private void AddTransparencyMenuItem(ToolStripMenuItem parent, string name, int value)
475 | {
476 | var item = new ToolStripMenuItem(name, null, (s, e) => {
477 | ChangeLineTransparency(value);
478 | });
479 | item.Checked = (value == lineOpacity);
480 | parent.DropDownItems.Add(item);
481 | }
482 |
483 | private void ToggleHotkeyBinding(int index)
484 | {
485 | if (index >= 0 && index < 4)
486 | {
487 | bool newState = !hotkeyEnabled[index];
488 |
489 | if (newState)
490 | {
491 | // 尝试注册热键
492 | try
493 | {
494 | Keys key = Keys.F1 + index;
495 | bool onSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index, MOD_CONTROL, (int)key);
496 | bool offSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index, MOD_CONTROL | MOD_SHIFT, (int)key);
497 |
498 | if (onSuccess && offSuccess)
499 | {
500 | hotkeyEnabled[index] = true;
501 | }
502 | else
503 | {
504 | MessageBox.Show($"热键 Ctrl+F{index + 1} 注册失败,可能已被其他程序占用。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
505 | }
506 | }
507 | catch (Exception ex)
508 | {
509 | MessageBox.Show($"注册热键时发生错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
510 | }
511 | }
512 | else
513 | {
514 | // 注销热键
515 | UnregisterHotkeyPair(index);
516 | hotkeyEnabled[index] = false;
517 |
518 | // 如果有对应的竖线,则移除它
519 | if (verticalLines.ContainsKey(index))
520 | {
521 | verticalLines[index].Close();
522 | verticalLines.Remove(index);
523 | lineStates[index] = false;
524 | }
525 | }
526 |
527 | UpdateHotkeyMenuCheckedState();
528 | SaveConfig();
529 | }
530 | }
531 |
532 | private void UpdateHotkeyMenuCheckedState()
533 | {
534 | if (trayIcon?.ContextMenuStrip == null) return;
535 |
536 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
537 | {
538 | if (item is ToolStripMenuItem menuItem && menuItem.Text == "持续竖线")
539 | {
540 | var hotkeyMenu = menuItem.DropDownItems[0] as ToolStripMenuItem;
541 | if (hotkeyMenu != null)
542 | {
543 | for (int i = 0; i < hotkeyMenu.DropDownItems.Count; i++)
544 | {
545 | if (hotkeyMenu.DropDownItems[i] is ToolStripMenuItem subItem)
546 | {
547 | subItem.Checked = hotkeyEnabled[i];
548 | }
549 | }
550 | }
551 | break;
552 | }
553 | }
554 | }
555 |
556 | private void ChangeLineThickness(int thickness)
557 | {
558 | lineWidth = thickness;
559 | foreach (var line in verticalLines.Values)
560 | {
561 | line.Width = thickness;
562 | }
563 | UpdateThicknessMenuCheckedState();
564 | SaveConfig();
565 | }
566 |
567 | private void ChangeLineColor(Color color)
568 | {
569 | lineColor = color;
570 | foreach (var line in verticalLines.Values)
571 | {
572 | line.BackColor = color;
573 | }
574 | UpdateColorMenuCheckedState();
575 | SaveConfig();
576 | }
577 |
578 | private void ChangeLineTransparency(int value)
579 | {
580 | lineOpacity = value;
581 | double opacity = value / 100.0;
582 | foreach (var line in verticalLines.Values)
583 | {
584 | line.Opacity = opacity;
585 | }
586 | UpdateTransparencyMenuCheckedState();
587 | SaveConfig();
588 | }
589 |
590 | private void UpdateThicknessMenuCheckedState()
591 | {
592 | UpdateMenuCheckedState("竖线粗细", item => {
593 | string thicknessStr = lineWidth.ToString();
594 | return item.Text.Contains(thicknessStr);
595 | });
596 | }
597 |
598 | private void UpdateColorMenuCheckedState()
599 | {
600 | UpdateMenuCheckedState("竖线颜色", item => {
601 | if (item.Image is Bitmap bmp)
602 | {
603 | try
604 | {
605 | Color menuColor = bmp.GetPixel(8, 8);
606 | return Math.Abs(menuColor.R - lineColor.R) < 5 &&
607 | Math.Abs(menuColor.G - lineColor.G) < 5 &&
608 | Math.Abs(menuColor.B - lineColor.B) < 5;
609 | }
610 | catch
611 | {
612 | return false;
613 | }
614 | }
615 | return false;
616 | });
617 | }
618 |
619 | private void UpdateTransparencyMenuCheckedState()
620 | {
621 | UpdateMenuCheckedState("竖线透明度", item => item.Text.Contains(lineOpacity.ToString() + "%"));
622 | }
623 |
624 | private void UpdateMenuCheckedState(string menuName, Func checkCondition)
625 | {
626 | if (trayIcon?.ContextMenuStrip == null) return;
627 |
628 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
629 | {
630 | if (item is ToolStripMenuItem verticalMenu && verticalMenu.Text == "持续竖线")
631 | {
632 | foreach (ToolStripItem subItem in verticalMenu.DropDownItems)
633 | {
634 | if (subItem is ToolStripMenuItem targetMenu && targetMenu.Text == menuName)
635 | {
636 | foreach (ToolStripItem optionItem in targetMenu.DropDownItems)
637 | {
638 | if (optionItem is ToolStripMenuItem menuItem)
639 | {
640 | menuItem.Checked = checkCondition(menuItem);
641 | }
642 | }
643 | break;
644 | }
645 | }
646 | break;
647 | }
648 | }
649 | }
650 |
651 | protected override void WndProc(ref Message m)
652 | {
653 | if (m.Msg == WM_HOTKEY)
654 | {
655 | int hotkeyId = m.WParam.ToInt32();
656 |
657 | // 处理开启热键 (Ctrl+F1-F4)
658 | if (hotkeyId >= BASE_HOTKEY_ID_ON && hotkeyId < BASE_HOTKEY_ID_ON + 4)
659 | {
660 | int lineIndex = hotkeyId - BASE_HOTKEY_ID_ON;
661 | if (hotkeyEnabled[lineIndex])
662 | {
663 | ShowVerticalLine(lineIndex);
664 | }
665 | }
666 | // 处理关闭热键 (Ctrl+Shift+F1-F4)
667 | else if (hotkeyId >= BASE_HOTKEY_ID_OFF && hotkeyId < BASE_HOTKEY_ID_OFF + 4)
668 | {
669 | int lineIndex = hotkeyId - BASE_HOTKEY_ID_OFF;
670 | if (hotkeyEnabled[lineIndex])
671 | {
672 | HideVerticalLine(lineIndex);
673 | }
674 | }
675 | }
676 | base.WndProc(ref m);
677 | }
678 |
679 | private void ShowVerticalLine(int index)
680 | {
681 | Screen currentScreen = Screen.FromPoint(Cursor.Position);
682 |
683 | // 如果这条线已经存在,就更新它的位置
684 | if (verticalLines.ContainsKey(index))
685 | {
686 | DraggableVerticalLine line = verticalLines[index];
687 | line.Location = new Point(Cursor.Position.X - (lineWidth / 2), currentScreen.Bounds.Y);
688 |
689 | // 确保现有线条保持置顶
690 | if (line.Handle != IntPtr.Zero)
691 | {
692 | line.TopMost = true;
693 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
694 | }
695 | }
696 | else
697 | {
698 | // 创建新的竖线
699 | DraggableVerticalLine line = new DraggableVerticalLine(lineWidth, currentScreen.Bounds.Height, lineColor, lineOpacity / 100.0, mouseClickThrough)
700 | {
701 | StartPosition = FormStartPosition.Manual,
702 | Location = new Point(
703 | Cursor.Position.X - (lineWidth / 2),
704 | currentScreen.Bounds.Y
705 | ),
706 | TopMost = true // 创建时就设置为置顶
707 | };
708 |
709 | line.Location = new Point(Cursor.Position.X - (lineWidth / 2), currentScreen.Bounds.Y);
710 | line.Show();
711 |
712 | // → 关键:Show() 后立刻重置 Width,绕过 DPI 放大
713 | line.Width = lineWidth;
714 |
715 | // 强制使用Windows API置顶
716 | if (line.Handle != IntPtr.Zero)
717 | {
718 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
719 | BringWindowToTop(line.Handle);
720 | }
721 |
722 | verticalLines[index] = line;
723 | }
724 |
725 | lineStates[index] = true;
726 | }
727 |
728 | private void HideVerticalLine(int index)
729 | {
730 | if (verticalLines.ContainsKey(index))
731 | {
732 | verticalLines[index].Close();
733 | verticalLines.Remove(index);
734 | lineStates[index] = false;
735 | }
736 | }
737 |
738 | protected override void OnFormClosing(FormClosingEventArgs e)
739 | {
740 | // 注销所有热键
741 | for (int i = 0; i < 4; i++)
742 | {
743 | if (hotkeyEnabled[i])
744 | {
745 | UnregisterHotkeyPair(i);
746 | }
747 | }
748 |
749 | // 关闭所有竖线 - 创建副本避免集合修改异常
750 | var linesToClose = new List(verticalLines.Values);
751 | foreach (var line in linesToClose)
752 | {
753 | try
754 | {
755 | if (line != null && !line.IsDisposed)
756 | {
757 | line.Close();
758 | }
759 | }
760 | catch (Exception)
761 | {
762 | // 忽略关闭时的异常
763 | }
764 | }
765 | verticalLines.Clear();
766 | lineStates.Clear();
767 |
768 | base.OnFormClosing(e);
769 | }
770 |
771 | ///
772 | /// 显示所有竖线
773 | ///
774 | public void ShowAllLines()
775 | {
776 | foreach (var line in verticalLines.Values)
777 | {
778 | if (!line.Visible)
779 | {
780 | line.Show();
781 | }
782 | }
783 | }
784 |
785 | ///
786 | /// 隐藏所有竖线
787 | ///
788 | public void HideAllLines()
789 | {
790 | foreach (var line in verticalLines.Values)
791 | {
792 | if (line.Visible)
793 | {
794 | line.Hide();
795 | }
796 | }
797 | }
798 |
799 | ///
800 | /// 关闭所有竖线(彻底移除)
801 | ///
802 | public void CloseAllLines()
803 | {
804 | var linesToClose = new List(verticalLines.Keys);
805 | foreach (int index in linesToClose)
806 | {
807 | if (verticalLines.ContainsKey(index))
808 | {
809 | verticalLines[index].Close();
810 | verticalLines.Remove(index);
811 | lineStates[index] = false;
812 | }
813 | }
814 | }
815 |
816 | ///
817 | /// 重新置顶所有竖线
818 | ///
819 | public void BringAllLinesToTop()
820 | {
821 | foreach (var line in verticalLines.Values)
822 | {
823 | if (line != null && !line.IsDisposed)
824 | {
825 | line.TopMost = false;
826 | line.TopMost = true;
827 | line.BringToFront();
828 | }
829 | }
830 | }
831 |
832 | ///
833 | /// 确保所有竖线保持置顶状态(用于持续置顶功能) - 与其他置顶程序抢夺置顶权
834 | ///
835 | public void EnsureTopmost()
836 | {
837 | foreach (var line in verticalLines.Values)
838 | {
839 | if (line != null && !line.IsDisposed && line.Visible && line.Handle != IntPtr.Zero)
840 | {
841 | // 强制重新设置置顶状态,抢夺置顶权
842 | line.TopMost = false; // 先取消置顶
843 | line.TopMost = true; // 再重新置顶,抢夺置顶权
844 |
845 | // 使用Windows API强制置顶并显示
846 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
847 | BringWindowToTop(line.Handle);
848 | }
849 | }
850 | }
851 |
852 | ///
853 | /// 启用所有热键
854 | ///
855 | public void EnableAllHotkeys()
856 | {
857 | for (int i = 0; i < hotkeyEnabled.Length; i++)
858 | {
859 | if (hotkeyEnabled[i])
860 | {
861 | RegisterHotkeyPair(i);
862 | }
863 | }
864 | }
865 |
866 | ///
867 | /// 禁用所有热键
868 | ///
869 | public void DisableAllHotkeys()
870 | {
871 | for (int i = 0; i < hotkeyEnabled.Length; i++)
872 | {
873 | if (hotkeyEnabled[i])
874 | {
875 | UnregisterHotkeyPair(i);
876 | }
877 | }
878 | }
879 |
880 | // 加载配置
881 | private void LoadConfig()
882 | {
883 | try
884 | {
885 | if (File.Exists(configPath))
886 | {
887 | string jsonString = File.ReadAllText(configPath);
888 | var config = JsonSerializer.Deserialize(jsonString);
889 |
890 | // 确保热键数组长度正确
891 | if (config.HotkeyEnabled != null && config.HotkeyEnabled.Length == 4)
892 | {
893 | hotkeyEnabled = config.HotkeyEnabled;
894 | }
895 | else
896 | {
897 | hotkeyEnabled = new bool[] { true, false, false, false };
898 | }
899 |
900 | lineWidth = config.LineWidth;
901 | lineColor = ColorTranslator.FromHtml(config.LineColor);
902 | lineOpacity = config.LineOpacity;
903 | mouseClickThrough = config.MouseClickThrough;
904 | }
905 | }
906 | catch (Exception)
907 | {
908 | // 如果加载失败,使用默认值
909 | hotkeyEnabled = new bool[] { true, false, false, false };
910 | lineWidth = 1;
911 | lineColor = Color.Blue;
912 | lineOpacity = 100;
913 | mouseClickThrough = true;
914 | }
915 | }
916 |
917 | // 保存配置
918 | private void SaveConfig()
919 | {
920 | try
921 | {
922 | var config = new Config
923 | {
924 | HotkeyEnabled = hotkeyEnabled,
925 | LineWidth = lineWidth,
926 | LineColor = ColorTranslator.ToHtml(lineColor),
927 | LineOpacity = lineOpacity,
928 | MouseClickThrough = mouseClickThrough
929 | };
930 |
931 | string jsonString = JsonSerializer.Serialize(config, new JsonSerializerOptions
932 | {
933 | WriteIndented = true
934 | });
935 |
936 | var configDir = Path.GetDirectoryName(configPath);
937 | if (!Directory.Exists(configDir))
938 | {
939 | Directory.CreateDirectory(configDir);
940 | }
941 |
942 | File.WriteAllText(configPath, jsonString);
943 | }
944 | catch (Exception ex)
945 | {
946 | MessageBox.Show($"保存竖线配置时出错: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
947 | }
948 | }
949 | }
950 | }
--------------------------------------------------------------------------------
/Line/HorizontalLineForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using System.Windows.Forms;
4 | using System.Runtime.InteropServices;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Text.Json;
8 | using System.Reflection;
9 | using System.Linq;
10 |
11 | namespace Line
12 | {
13 | // 可拖拽的横线类
14 | public class DraggableHorizontalLine : Form
15 | {
16 | private bool isDragging = false;
17 | private Point lastCursor;
18 | private bool mouseClickThrough;
19 |
20 | // Windows API for mouse click-through
21 | private const int WS_EX_TRANSPARENT = 0x20;
22 | private const int WS_EX_LAYERED = 0x80000;
23 | private const int WS_EX_NOACTIVATE = 0x08000000;
24 | private const int GWL_EXSTYLE = (-20);
25 |
26 | // 新增:窗口消息常量
27 | private const int WM_SETCURSOR = 0x0020;
28 | private const int WM_MOUSEACTIVATE = 0x0021;
29 | private const int MA_NOACTIVATE = 3;
30 |
31 | [DllImport("user32.dll")]
32 | private static extern int GetWindowLong(IntPtr hwnd, int index);
33 |
34 | [DllImport("user32.dll")]
35 | private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
36 |
37 | public DraggableHorizontalLine(int width, int height, Color color, double opacity, bool clickThrough)
38 | {
39 | this.FormBorderStyle = FormBorderStyle.None;
40 | this.ShowInTaskbar = false;
41 | this.TopMost = true;
42 | this.BackColor = color;
43 | this.TransparencyKey = Color.Black;
44 | this.Opacity = opacity;
45 | this.Width = width;
46 | this.Height = height;
47 | this.AutoScaleMode = AutoScaleMode.None;
48 | this.StartPosition = FormStartPosition.Manual;
49 |
50 | mouseClickThrough = clickThrough;
51 |
52 | // 如果不是鼠标穿透模式,添加拖拽事件和设置光标
53 | if (!mouseClickThrough)
54 | {
55 | this.MouseDown += OnMouseDown;
56 | this.MouseMove += OnMouseMove;
57 | this.MouseUp += OnMouseUp;
58 | this.Cursor = Cursors.SizeNS; // 设置为垂直调整大小光标
59 | }
60 | }
61 |
62 | // 重写CreateParams,在创建窗口时就设置所有必要的扩展样式
63 | protected override CreateParams CreateParams
64 | {
65 | get
66 | {
67 | var cp = base.CreateParams;
68 | // 分层 + 点透 + 不激活
69 | cp.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
70 | return cp;
71 | }
72 | }
73 |
74 | protected override void OnHandleCreated(EventArgs e)
75 | {
76 | base.OnHandleCreated(e);
77 | // 强制保证都加上去
78 | int ex = GetWindowLong(this.Handle, GWL_EXSTYLE);
79 | ex |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
80 | SetWindowLong(this.Handle, GWL_EXSTYLE, ex);
81 | }
82 |
83 | public void SetClickThrough(bool enable)
84 | {
85 | mouseClickThrough = enable;
86 | if (this.Handle != IntPtr.Zero)
87 | {
88 | if (enable)
89 | {
90 | // 启用穿透模式:移除拖拽事件,不设置光标
91 | this.MouseDown -= OnMouseDown;
92 | this.MouseMove -= OnMouseMove;
93 | this.MouseUp -= OnMouseUp;
94 | // 不再设置 this.Cursor,让系统决定光标形状
95 | }
96 | else
97 | {
98 | // 禁用穿透模式:移除透明样式,添加拖拽事件
99 | int exStyle = GetWindowLong(this.Handle, GWL_EXSTYLE);
100 | SetWindowLong(this.Handle, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
101 | this.Cursor = Cursors.SizeNS;
102 | // 添加拖拽事件
103 | this.MouseDown -= OnMouseDown; // 先移除避免重复
104 | this.MouseMove -= OnMouseMove;
105 | this.MouseUp -= OnMouseUp;
106 | this.MouseDown += OnMouseDown;
107 | this.MouseMove += OnMouseMove;
108 | this.MouseUp += OnMouseUp;
109 | }
110 | }
111 | }
112 |
113 | // 拦截窗口消息,解决光标和激活问题
114 | protected override void WndProc(ref Message m)
115 | {
116 | if (mouseClickThrough)
117 | {
118 | if (m.Msg == WM_MOUSEACTIVATE)
119 | {
120 | // 不激活自己,直接交给下面窗口
121 | m.Result = new IntPtr(MA_NOACTIVATE);
122 | return;
123 | }
124 | if (m.Msg == WM_SETCURSOR)
125 | {
126 | // 不处理,让系统去给下面窗口设光标
127 | return;
128 | }
129 | }
130 | base.WndProc(ref m);
131 | }
132 |
133 | private void OnMouseDown(object sender, MouseEventArgs e)
134 | {
135 | if (e.Button == MouseButtons.Left)
136 | {
137 | isDragging = true;
138 | lastCursor = Cursor.Position;
139 | }
140 | }
141 |
142 | private void OnMouseMove(object sender, MouseEventArgs e)
143 | {
144 | if (isDragging)
145 | {
146 | Point currentCursor = Cursor.Position;
147 | int deltaY = currentCursor.Y - lastCursor.Y;
148 |
149 | this.Location = new Point(this.Location.X, this.Location.Y + deltaY);
150 | lastCursor = currentCursor;
151 | }
152 | }
153 |
154 | private void OnMouseUp(object sender, MouseEventArgs e)
155 | {
156 | if (e.Button == MouseButtons.Left)
157 | {
158 | isDragging = false;
159 | }
160 | }
161 | }
162 |
163 | public class HorizontalLineForm : Form
164 | {
165 | private const int WM_HOTKEY = 0x0312;
166 | private Dictionary> horizontalLines = new Dictionary>();
167 | private Dictionary lineStates = new Dictionary();
168 | private NotifyIcon trayIcon;
169 |
170 | // 线条默认高度为1像素
171 | private int lineHeight = 1;
172 |
173 | // 线条颜色,默认为绿色
174 | private Color lineColor = Color.Green;
175 |
176 | // 线条透明度,默认为100%
177 | private int lineOpacity = 100;
178 |
179 | // 鼠标穿透设置
180 | private bool mouseClickThrough = true;
181 |
182 | // 显示模式:false=仅当前屏幕,true=全部屏幕
183 | private bool showOnAllScreens = false;
184 |
185 | // 热键ID基础值(1-4用于开启,5-8用于关闭)
186 | private const int BASE_HOTKEY_ID_ON = 300;
187 | private const int BASE_HOTKEY_ID_OFF = 400;
188 |
189 | // 热键绑定状态
190 | private bool[] hotkeyEnabled = new bool[] { true, true, false, false }; // 默认启用前两组
191 |
192 | // 用于处理初始显示的标志
193 | private bool isFirstShow = true;
194 |
195 | // 修改配置文件路径
196 | private readonly string configPath = Path.Combine(
197 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
198 | "ScreenLine",
199 | "horizontal_config.json"
200 | );
201 |
202 | // 配置类
203 | private class Config
204 | {
205 | public bool[] HotkeyEnabled { get; set; }
206 | public int LineHeight { get; set; }
207 | public string LineColor { get; set; }
208 | public int LineOpacity { get; set; }
209 | public bool MouseClickThrough { get; set; }
210 | public bool ShowOnAllScreens { get; set; }
211 | }
212 |
213 | [DllImport("user32.dll")]
214 | private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
215 |
216 | [DllImport("user32.dll")]
217 | private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
218 |
219 | // 增强的置顶Windows API
220 | [DllImport("user32.dll")]
221 | private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
222 |
223 | [DllImport("user32.dll")]
224 | private static extern bool BringWindowToTop(IntPtr hWnd);
225 |
226 | // 置顶相关常量
227 | private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
228 | private const uint SWP_NOMOVE = 0x0002;
229 | private const uint SWP_NOSIZE = 0x0001;
230 | private const uint SWP_SHOWWINDOW = 0x0040;
231 |
232 | // 修饰键常量
233 | private const int MOD_CONTROL = 0x0002;
234 | private const int MOD_SHIFT = 0x0004;
235 |
236 | public HorizontalLineForm(NotifyIcon existingTrayIcon)
237 | {
238 | this.AutoScaleMode = AutoScaleMode.None;
239 | this.trayIcon = existingTrayIcon;
240 |
241 | // 加载配置
242 | LoadConfig();
243 |
244 | InitializeComponent();
245 | InitializeHotkeys();
246 | AddHorizontalLineMenuItems();
247 |
248 | // 设置初始鼠标穿透状态
249 | SetClickThrough(mouseClickThrough);
250 |
251 | // 显示一次初始横线,然后立即隐藏
252 | ShowInitialLine();
253 | }
254 |
255 | private void ShowInitialLine()
256 | {
257 | if (isFirstShow)
258 | {
259 | // 创建并显示一个临时的横线
260 | DraggableHorizontalLine tempLine = new DraggableHorizontalLine(
261 | Screen.PrimaryScreen.Bounds.Width,
262 | lineHeight,
263 | lineColor,
264 | lineOpacity / 100.0,
265 | mouseClickThrough
266 | );
267 |
268 | tempLine.Show();
269 | tempLine.Close();
270 | isFirstShow = false;
271 | }
272 |
273 | // 隐藏主窗体
274 | this.Opacity = 0;
275 | }
276 |
277 | private void InitializeComponent()
278 | {
279 | this.FormBorderStyle = FormBorderStyle.None;
280 | this.ShowInTaskbar = false;
281 | this.TopMost = true;
282 | this.BackColor = lineColor;
283 | this.TransparencyKey = Color.Black;
284 | this.AutoScaleMode = AutoScaleMode.None;
285 | this.Width = Screen.PrimaryScreen.Bounds.Width;
286 | this.Height = lineHeight;
287 | }
288 |
289 | private void InitializeHotkeys()
290 | {
291 | // 根据配置注册所有启用的热键
292 | for (int i = 0; i < hotkeyEnabled.Length; i++)
293 | {
294 | if (hotkeyEnabled[i])
295 | {
296 | RegisterHotkeyPair(i);
297 | }
298 | }
299 | }
300 |
301 | private void RegisterHotkeyPair(int index)
302 | {
303 | try
304 | {
305 | if (index >= 0 && index < 4)
306 | {
307 | Keys key = Keys.D1 + index; // 数字键1-4
308 | // 注册 Ctrl+1-4
309 | bool onSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index, MOD_CONTROL, (int)key);
310 | // 注册 Ctrl+Shift+1-4
311 | bool offSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index, MOD_CONTROL | MOD_SHIFT, (int)key);
312 |
313 | // 如果注册失败,更新状态并保存配置
314 | if (!onSuccess || !offSuccess)
315 | {
316 | hotkeyEnabled[index] = false;
317 | UpdateHotkeyMenuCheckedState();
318 | SaveConfig();
319 | MessageBox.Show($"热键 Ctrl+{index + 1} 注册失败,可能已被其他程序占用。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
320 | }
321 | }
322 | }
323 | catch (Exception ex)
324 | {
325 | hotkeyEnabled[index] = false;
326 | UpdateHotkeyMenuCheckedState();
327 | SaveConfig();
328 | MessageBox.Show($"注册热键时发生错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
329 | }
330 | }
331 |
332 | private void UnregisterHotkeyPair(int index)
333 | {
334 | if (index >= 0 && index < 4)
335 | {
336 | UnregisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index);
337 | UnregisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index);
338 | }
339 | }
340 |
341 | private void SetClickThrough(bool enable)
342 | {
343 | mouseClickThrough = enable;
344 | foreach (var linesList in horizontalLines.Values)
345 | {
346 | foreach (var line in linesList)
347 | {
348 | line.SetClickThrough(enable);
349 | }
350 | }
351 | SaveConfig();
352 | }
353 |
354 | private void AddHorizontalLineMenuItems()
355 | {
356 | if (trayIcon?.ContextMenuStrip == null) return;
357 |
358 | ToolStripMenuItem horizontalLineMenu = new ToolStripMenuItem("持续横线");
359 |
360 | // 热键绑定菜单
361 | ToolStripMenuItem hotkeyBindingMenu = new ToolStripMenuItem("热键绑定");
362 | for (int i = 0; i < 4; i++)
363 | {
364 | int index = i;
365 | var item = new ToolStripMenuItem($"Ctrl+{i + 1}/Ctrl+Shift+{i + 1}", null, (s, e) => {
366 | ToggleHotkeyBinding(index);
367 | });
368 | item.Checked = hotkeyEnabled[i];
369 | hotkeyBindingMenu.DropDownItems.Add(item);
370 | }
371 |
372 | // 显示模式菜单
373 | ToolStripMenuItem displayModeMenu = new ToolStripMenuItem("显示模式");
374 | var currentScreenItem = new ToolStripMenuItem("仅鼠标所在屏幕", null, (s, e) => {
375 | showOnAllScreens = false;
376 | if (s is ToolStripMenuItem menuItem)
377 | {
378 | menuItem.Checked = true;
379 | // 更新另一个选项的状态
380 | foreach (ToolStripMenuItem item in displayModeMenu.DropDownItems)
381 | {
382 | if (item.Text == "所有屏幕")
383 | {
384 | item.Checked = false;
385 | break;
386 | }
387 | }
388 | }
389 | SaveConfig();
390 | });
391 | currentScreenItem.Checked = !showOnAllScreens;
392 |
393 | var allScreensItem = new ToolStripMenuItem("所有屏幕", null, (s, e) => {
394 | showOnAllScreens = true;
395 | if (s is ToolStripMenuItem menuItem)
396 | {
397 | menuItem.Checked = true;
398 | // 更新另一个选项的状态
399 | foreach (ToolStripMenuItem item in displayModeMenu.DropDownItems)
400 | {
401 | if (item.Text == "仅鼠标所在屏幕")
402 | {
403 | item.Checked = false;
404 | break;
405 | }
406 | }
407 | }
408 | SaveConfig();
409 | });
410 | allScreensItem.Checked = showOnAllScreens;
411 |
412 | displayModeMenu.DropDownItems.Add(currentScreenItem);
413 | displayModeMenu.DropDownItems.Add(allScreensItem);
414 |
415 | // 鼠标穿透选项
416 | var mousePenetrationItem = new ToolStripMenuItem("鼠标穿透", null, (s, e) => {
417 | SetClickThrough(!mouseClickThrough);
418 | if (s is ToolStripMenuItem menuItem)
419 | {
420 | menuItem.Checked = mouseClickThrough;
421 | }
422 | });
423 | mousePenetrationItem.Checked = mouseClickThrough;
424 |
425 | // 线条粗细菜单
426 | ToolStripMenuItem lineThicknessItem = new ToolStripMenuItem("横线粗细");
427 | AddThicknessMenuItem(lineThicknessItem, "细线 (1像素)", 1);
428 | AddThicknessMenuItem(lineThicknessItem, "中等 (2像素)", 2);
429 | AddThicknessMenuItem(lineThicknessItem, "粗线 (3像素)", 3);
430 | AddThicknessMenuItem(lineThicknessItem, "很粗 (5像素)", 5);
431 |
432 | // 线条颜色菜单
433 | ToolStripMenuItem lineColorItem = new ToolStripMenuItem("横线颜色");
434 | AddColorMenuItem(lineColorItem, "红色", Color.Red);
435 | AddColorMenuItem(lineColorItem, "绿色", Color.Green);
436 | AddColorMenuItem(lineColorItem, "蓝色", Color.Blue);
437 | AddColorMenuItem(lineColorItem, "黄色", Color.Yellow);
438 | AddColorMenuItem(lineColorItem, "橙色", Color.Orange);
439 | AddColorMenuItem(lineColorItem, "紫色", Color.Purple);
440 | AddColorMenuItem(lineColorItem, "青色", Color.Cyan);
441 | AddColorMenuItem(lineColorItem, "黑色", Color.FromArgb(1, 1, 1));
442 | AddColorMenuItem(lineColorItem, "白色", Color.White);
443 |
444 | // 透明度菜单
445 | ToolStripMenuItem transparencyItem = new ToolStripMenuItem("横线透明度");
446 | AddTransparencyMenuItem(transparencyItem, "100% (不透明)", 100);
447 | AddTransparencyMenuItem(transparencyItem, "75%", 75);
448 | AddTransparencyMenuItem(transparencyItem, "50%", 50);
449 | AddTransparencyMenuItem(transparencyItem, "25%", 25);
450 |
451 | // 添加所有子菜单
452 | horizontalLineMenu.DropDownItems.Add(hotkeyBindingMenu);
453 | horizontalLineMenu.DropDownItems.Add(displayModeMenu);
454 | horizontalLineMenu.DropDownItems.Add(mousePenetrationItem);
455 | horizontalLineMenu.DropDownItems.Add(lineThicknessItem);
456 | horizontalLineMenu.DropDownItems.Add(lineColorItem);
457 | horizontalLineMenu.DropDownItems.Add(transparencyItem);
458 |
459 | // 在竖线菜单之后插入横线菜单
460 | int insertIndex = -1;
461 | for (int i = 0; i < trayIcon.ContextMenuStrip.Items.Count; i++)
462 | {
463 | if (trayIcon.ContextMenuStrip.Items[i] is ToolStripMenuItem menuItem &&
464 | menuItem.Text == "竖线设置")
465 | {
466 | insertIndex = i + 1;
467 | break;
468 | }
469 | }
470 |
471 | if (insertIndex != -1)
472 | {
473 | trayIcon.ContextMenuStrip.Items.Insert(insertIndex, horizontalLineMenu);
474 | }
475 | else
476 | {
477 | // 如果找不到竖线菜单,就在分隔符前插入
478 | int separatorIndex = -1;
479 | for (int i = 0; i < trayIcon.ContextMenuStrip.Items.Count; i++)
480 | {
481 | if (trayIcon.ContextMenuStrip.Items[i] is ToolStripSeparator)
482 | {
483 | separatorIndex = i;
484 | break;
485 | }
486 | }
487 |
488 | if (separatorIndex != -1)
489 | {
490 | trayIcon.ContextMenuStrip.Items.Insert(separatorIndex, horizontalLineMenu);
491 | }
492 | else
493 | {
494 | trayIcon.ContextMenuStrip.Items.Add(horizontalLineMenu);
495 | }
496 | }
497 | }
498 |
499 | private void AddThicknessMenuItem(ToolStripMenuItem parent, string text, int thickness)
500 | {
501 | var item = new ToolStripMenuItem(text, null, (s, e) => {
502 | ChangeLineThickness(thickness);
503 | });
504 | item.Checked = (thickness == lineHeight);
505 | parent.DropDownItems.Add(item);
506 | }
507 |
508 | private void AddColorMenuItem(ToolStripMenuItem parent, string name, Color color)
509 | {
510 | Bitmap colorPreview = new Bitmap(16, 16);
511 | using (Graphics g = Graphics.FromImage(colorPreview))
512 | {
513 | g.FillRectangle(new SolidBrush(color), 0, 0, 16, 16);
514 | g.DrawRectangle(Pens.Gray, 0, 0, 15, 15);
515 | }
516 |
517 | var item = new ToolStripMenuItem(name, colorPreview, (s, e) => {
518 | ChangeLineColor(color);
519 | });
520 | item.Checked = color.Equals(lineColor);
521 | parent.DropDownItems.Add(item);
522 | }
523 |
524 | private void AddTransparencyMenuItem(ToolStripMenuItem parent, string name, int value)
525 | {
526 | var item = new ToolStripMenuItem(name, null, (s, e) => {
527 | ChangeLineTransparency(value);
528 | });
529 | item.Checked = (value == lineOpacity);
530 | parent.DropDownItems.Add(item);
531 | }
532 |
533 | private void ToggleHotkeyBinding(int index)
534 | {
535 | if (index >= 0 && index < 4)
536 | {
537 | bool newState = !hotkeyEnabled[index];
538 |
539 | if (newState)
540 | {
541 | // 尝试注册热键
542 | try
543 | {
544 | Keys key = Keys.D1 + index;
545 | bool onSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_ON + index, MOD_CONTROL, (int)key);
546 | bool offSuccess = RegisterHotKey(this.Handle, BASE_HOTKEY_ID_OFF + index, MOD_CONTROL | MOD_SHIFT, (int)key);
547 |
548 | if (onSuccess && offSuccess)
549 | {
550 | hotkeyEnabled[index] = true;
551 | }
552 | else
553 | {
554 | MessageBox.Show($"热键 Ctrl+{index + 1} 注册失败,可能已被其他程序占用。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
555 | }
556 | }
557 | catch (Exception ex)
558 | {
559 | MessageBox.Show($"注册热键时发生错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
560 | }
561 | }
562 | else
563 | {
564 | // 注销热键
565 | UnregisterHotkeyPair(index);
566 | hotkeyEnabled[index] = false;
567 |
568 | // 如果有对应的横线,则移除它
569 | if (horizontalLines.ContainsKey(index))
570 | {
571 | foreach (var line in horizontalLines[index])
572 | {
573 | line.Close();
574 | }
575 | horizontalLines.Remove(index);
576 | lineStates[index] = false;
577 | }
578 | }
579 |
580 | UpdateHotkeyMenuCheckedState();
581 | SaveConfig();
582 | }
583 | }
584 |
585 | private void UpdateHotkeyMenuCheckedState()
586 | {
587 | if (trayIcon?.ContextMenuStrip == null) return;
588 |
589 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
590 | {
591 | if (item is ToolStripMenuItem menuItem && menuItem.Text == "持续横线")
592 | {
593 | var hotkeyMenu = menuItem.DropDownItems[0] as ToolStripMenuItem;
594 | if (hotkeyMenu != null)
595 | {
596 | for (int i = 0; i < hotkeyMenu.DropDownItems.Count; i++)
597 | {
598 | if (hotkeyMenu.DropDownItems[i] is ToolStripMenuItem subItem)
599 | {
600 | subItem.Checked = hotkeyEnabled[i];
601 | }
602 | }
603 | }
604 | break;
605 | }
606 | }
607 | }
608 |
609 | private void ChangeLineThickness(int thickness)
610 | {
611 | lineHeight = thickness;
612 |
613 | // 更新所有横线的高度
614 | foreach (var linesList in horizontalLines.Values)
615 | {
616 | foreach (var line in linesList)
617 | {
618 | line.Height = lineHeight;
619 | }
620 | }
621 |
622 | // 更新菜单项选中状态
623 | UpdateThicknessMenuCheckedState();
624 | SaveConfig();
625 | }
626 |
627 | private void ChangeLineColor(Color color)
628 | {
629 | lineColor = color;
630 |
631 | // 更新所有横线的颜色
632 | foreach (var linesList in horizontalLines.Values)
633 | {
634 | foreach (var line in linesList)
635 | {
636 | line.BackColor = lineColor;
637 | }
638 | }
639 |
640 | // 更新菜单项选中状态
641 | UpdateColorMenuCheckedState();
642 | SaveConfig();
643 | }
644 |
645 | private void ChangeLineTransparency(int value)
646 | {
647 | lineOpacity = value;
648 |
649 | // 更新所有横线的透明度
650 | foreach (var linesList in horizontalLines.Values)
651 | {
652 | foreach (var line in linesList)
653 | {
654 | line.Opacity = lineOpacity / 100.0;
655 | }
656 | }
657 |
658 | // 更新菜单项选中状态
659 | UpdateTransparencyMenuCheckedState();
660 | SaveConfig();
661 | }
662 |
663 | private void UpdateThicknessMenuCheckedState()
664 | {
665 | UpdateMenuCheckedState("横线粗细", item => {
666 | string thicknessStr = lineHeight.ToString();
667 | return item.Text.Contains(thicknessStr);
668 | });
669 | }
670 |
671 | private void UpdateColorMenuCheckedState()
672 | {
673 | UpdateMenuCheckedState("横线颜色", item => {
674 | if (item.Image is Bitmap bmp)
675 | {
676 | try
677 | {
678 | Color menuColor = bmp.GetPixel(8, 8);
679 | return Math.Abs(menuColor.R - lineColor.R) < 5 &&
680 | Math.Abs(menuColor.G - lineColor.G) < 5 &&
681 | Math.Abs(menuColor.B - lineColor.B) < 5;
682 | }
683 | catch
684 | {
685 | return false;
686 | }
687 | }
688 | return false;
689 | });
690 | }
691 |
692 | private void UpdateTransparencyMenuCheckedState()
693 | {
694 | UpdateMenuCheckedState("横线透明度", item => item.Text.Contains(lineOpacity.ToString() + "%"));
695 | }
696 |
697 | private void UpdateMenuCheckedState(string menuName, Func checkCondition)
698 | {
699 | if (trayIcon?.ContextMenuStrip == null) return;
700 |
701 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
702 | {
703 | if (item is ToolStripMenuItem horizontalMenu && horizontalMenu.Text == "持续横线")
704 | {
705 | foreach (ToolStripItem subItem in horizontalMenu.DropDownItems)
706 | {
707 | if (subItem is ToolStripMenuItem targetMenu && targetMenu.Text == menuName)
708 | {
709 | foreach (ToolStripItem optionItem in targetMenu.DropDownItems)
710 | {
711 | if (optionItem is ToolStripMenuItem menuItem)
712 | {
713 | menuItem.Checked = checkCondition(menuItem);
714 | }
715 | }
716 | break;
717 | }
718 | }
719 | break;
720 | }
721 | }
722 | }
723 |
724 | protected override void WndProc(ref Message m)
725 | {
726 | if (m.Msg == WM_HOTKEY)
727 | {
728 | int hotkeyId = m.WParam.ToInt32();
729 |
730 | // 处理开启热键 (Ctrl+1-4)
731 | if (hotkeyId >= BASE_HOTKEY_ID_ON && hotkeyId < BASE_HOTKEY_ID_ON + 4)
732 | {
733 | int lineIndex = hotkeyId - BASE_HOTKEY_ID_ON;
734 | if (hotkeyEnabled[lineIndex])
735 | {
736 | ShowHorizontalLine(lineIndex);
737 | }
738 | }
739 | // 处理关闭热键 (Ctrl+Shift+1-4)
740 | else if (hotkeyId >= BASE_HOTKEY_ID_OFF && hotkeyId < BASE_HOTKEY_ID_OFF + 4)
741 | {
742 | int lineIndex = hotkeyId - BASE_HOTKEY_ID_OFF;
743 | if (hotkeyEnabled[lineIndex])
744 | {
745 | HideHorizontalLine(lineIndex);
746 | }
747 | }
748 | }
749 | base.WndProc(ref m);
750 | }
751 |
752 | private void ShowHorizontalLine(int index)
753 | {
754 | Point mousePos = Cursor.Position;
755 |
756 | if (showOnAllScreens)
757 | {
758 | // 在所有屏幕显示横线
759 | Screen mouseScreen = Screen.FromPoint(mousePos);
760 | int relativeY = mousePos.Y - mouseScreen.Bounds.Y;
761 |
762 | // 如果这条线已经存在,先关闭所有相关的线
763 | if (horizontalLines.ContainsKey(index))
764 | {
765 | foreach (var line in horizontalLines[index])
766 | {
767 | line.Close();
768 | }
769 | horizontalLines.Remove(index);
770 | }
771 |
772 | List firstLines = new List();
773 | // 为每个屏幕创建横线
774 | foreach (Screen screen in Screen.AllScreens)
775 | {
776 | int lineY = screen.Bounds.Y + relativeY;
777 | // 确保线条不超出屏幕边界
778 | if (lineY < screen.Bounds.Y) lineY = screen.Bounds.Y;
779 | if (lineY > screen.Bounds.Bottom - lineHeight) lineY = screen.Bounds.Bottom - lineHeight;
780 |
781 | DraggableHorizontalLine line = new DraggableHorizontalLine(
782 | screen.Bounds.Width,
783 | lineHeight,
784 | lineColor,
785 | lineOpacity / 100.0,
786 | mouseClickThrough
787 | )
788 | {
789 | TopMost = true // 创建时就设置为置顶
790 | };
791 |
792 | line.Location = new Point(screen.Bounds.X, lineY);
793 | line.Show();
794 | line.Height = lineHeight; // 重置高度以绕过DPI缩放
795 |
796 | // 强制使用Windows API置顶
797 | if (line.Handle != IntPtr.Zero)
798 | {
799 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
800 | BringWindowToTop(line.Handle);
801 | }
802 |
803 | firstLines.Add(line);
804 | }
805 |
806 | // 保存第一个屏幕的线条引用(用于管理)
807 | horizontalLines[index] = firstLines;
808 | }
809 | else
810 | {
811 | // 只在鼠标所在屏幕显示横线
812 | Screen currentScreen = Screen.FromPoint(mousePos);
813 |
814 | // 如果这条线已经存在,就更新它的位置
815 | if (horizontalLines.ContainsKey(index))
816 | {
817 | foreach (var line in horizontalLines[index])
818 | {
819 | line.Location = new Point(currentScreen.Bounds.X, mousePos.Y);
820 |
821 | // 确保现有线条保持置顶
822 | if (line.Handle != IntPtr.Zero)
823 | {
824 | line.TopMost = true;
825 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
826 | }
827 | }
828 | }
829 | else
830 | {
831 | // 创建新的横线 - 单屏幕模式只创建一条线
832 | List newLines = new List();
833 |
834 | DraggableHorizontalLine line = new DraggableHorizontalLine(
835 | currentScreen.Bounds.Width,
836 | lineHeight,
837 | lineColor,
838 | lineOpacity / 100.0,
839 | mouseClickThrough
840 | )
841 | {
842 | TopMost = true // 创建时就设置为置顶
843 | };
844 |
845 | line.Location = new Point(currentScreen.Bounds.X, mousePos.Y);
846 | line.Show();
847 | line.Height = lineHeight; // 重置高度以绕过DPI缩放
848 |
849 | // 强制使用Windows API置顶
850 | if (line.Handle != IntPtr.Zero)
851 | {
852 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
853 | BringWindowToTop(line.Handle);
854 | }
855 |
856 | newLines.Add(line);
857 | horizontalLines[index] = newLines;
858 | }
859 | }
860 |
861 | lineStates[index] = true;
862 | }
863 |
864 | private void HideHorizontalLine(int index)
865 | {
866 | if (horizontalLines.ContainsKey(index))
867 | {
868 | foreach (var line in horizontalLines[index])
869 | {
870 | line.Close();
871 | }
872 | horizontalLines.Remove(index);
873 | lineStates[index] = false;
874 | }
875 | }
876 |
877 | protected override void OnFormClosing(FormClosingEventArgs e)
878 | {
879 | // 注销所有热键
880 | for (int i = 0; i < 4; i++)
881 | {
882 | if (hotkeyEnabled[i])
883 | {
884 | UnregisterHotkeyPair(i);
885 | }
886 | }
887 |
888 | // 关闭所有横线 - 创建副本避免集合修改异常
889 | var linesToClose = new List>(horizontalLines.Values);
890 | foreach (var lines in linesToClose)
891 | {
892 | foreach (var line in lines)
893 | {
894 | try
895 | {
896 | if (line != null && !line.IsDisposed)
897 | {
898 | line.Close();
899 | }
900 | }
901 | catch (Exception)
902 | {
903 | // 忽略关闭时的异常
904 | }
905 | }
906 | }
907 | horizontalLines.Clear();
908 | lineStates.Clear();
909 |
910 | base.OnFormClosing(e);
911 | }
912 |
913 | ///
914 | /// 显示所有横线
915 | ///
916 | public void ShowAllLines()
917 | {
918 | foreach (var lines in horizontalLines.Values)
919 | {
920 | foreach (var line in lines)
921 | {
922 | if (!line.Visible)
923 | {
924 | line.Show();
925 | }
926 | }
927 | }
928 | }
929 |
930 | ///
931 | /// 隐藏所有横线
932 | ///
933 | public void HideAllLines()
934 | {
935 | foreach (var lines in horizontalLines.Values)
936 | {
937 | foreach (var line in lines)
938 | {
939 | if (line.Visible)
940 | {
941 | line.Hide();
942 | }
943 | }
944 | }
945 | }
946 |
947 | ///
948 | /// 关闭所有横线(彻底移除)
949 | ///
950 | public void CloseAllLines()
951 | {
952 | var linesToClose = new List(horizontalLines.Keys);
953 | foreach (int index in linesToClose)
954 | {
955 | if (horizontalLines.ContainsKey(index))
956 | {
957 | foreach (var line in horizontalLines[index])
958 | {
959 | line.Close();
960 | }
961 | horizontalLines.Remove(index);
962 | lineStates[index] = false;
963 | }
964 | }
965 | }
966 |
967 | ///
968 | /// 重新置顶所有横线
969 | ///
970 | public void BringAllLinesToTop()
971 | {
972 | foreach (var lines in horizontalLines.Values)
973 | {
974 | foreach (var line in lines)
975 | {
976 | if (line != null && !line.IsDisposed)
977 | {
978 | line.TopMost = false;
979 | line.TopMost = true;
980 | line.BringToFront();
981 | }
982 | }
983 | }
984 | }
985 |
986 | ///
987 | /// 确保所有横线保持置顶状态(用于持续置顶功能) - 与其他置顶程序抢夺置顶权
988 | ///
989 | public void EnsureTopmost()
990 | {
991 | foreach (var lines in horizontalLines.Values)
992 | {
993 | foreach (var line in lines)
994 | {
995 | if (line != null && !line.IsDisposed && line.Visible && line.Handle != IntPtr.Zero)
996 | {
997 | // 强制重新设置置顶状态,抢夺置顶权
998 | line.TopMost = false; // 先取消置顶
999 | line.TopMost = true; // 再重新置顶,抢夺置顶权
1000 |
1001 | // 使用Windows API强制置顶并显示
1002 | SetWindowPos(line.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
1003 | BringWindowToTop(line.Handle);
1004 | }
1005 | }
1006 | }
1007 | }
1008 |
1009 | ///
1010 | /// 启用所有热键
1011 | ///
1012 | public void EnableAllHotkeys()
1013 | {
1014 | for (int i = 0; i < hotkeyEnabled.Length; i++)
1015 | {
1016 | if (hotkeyEnabled[i])
1017 | {
1018 | RegisterHotkeyPair(i);
1019 | }
1020 | }
1021 | }
1022 |
1023 | ///
1024 | /// 禁用所有热键
1025 | ///
1026 | public void DisableAllHotkeys()
1027 | {
1028 | for (int i = 0; i < hotkeyEnabled.Length; i++)
1029 | {
1030 | if (hotkeyEnabled[i])
1031 | {
1032 | UnregisterHotkeyPair(i);
1033 | }
1034 | }
1035 | }
1036 |
1037 | // 加载配置
1038 | private void LoadConfig()
1039 | {
1040 | try
1041 | {
1042 | if (File.Exists(configPath))
1043 | {
1044 | string jsonString = File.ReadAllText(configPath);
1045 | var config = JsonSerializer.Deserialize(jsonString);
1046 |
1047 | // 确保热键数组长度正确
1048 | if (config.HotkeyEnabled != null && config.HotkeyEnabled.Length == 4)
1049 | {
1050 | hotkeyEnabled = config.HotkeyEnabled;
1051 | }
1052 | else
1053 | {
1054 | hotkeyEnabled = new bool[] { true, true, false, false };
1055 | }
1056 |
1057 | lineHeight = config.LineHeight;
1058 | lineColor = ColorTranslator.FromHtml(config.LineColor);
1059 | lineOpacity = config.LineOpacity;
1060 | mouseClickThrough = config.MouseClickThrough;
1061 | showOnAllScreens = config.ShowOnAllScreens;
1062 | }
1063 | }
1064 | catch (Exception)
1065 | {
1066 | // 如果加载失败,使用默认值
1067 | hotkeyEnabled = new bool[] { true, true, false, false };
1068 | lineHeight = 1;
1069 | lineColor = Color.Green;
1070 | lineOpacity = 100;
1071 | mouseClickThrough = true;
1072 | showOnAllScreens = false;
1073 | }
1074 | }
1075 |
1076 | // 保存配置
1077 | private void SaveConfig()
1078 | {
1079 | try
1080 | {
1081 | var config = new Config
1082 | {
1083 | HotkeyEnabled = hotkeyEnabled,
1084 | LineHeight = lineHeight,
1085 | LineColor = ColorTranslator.ToHtml(lineColor),
1086 | LineOpacity = lineOpacity,
1087 | MouseClickThrough = mouseClickThrough,
1088 | ShowOnAllScreens = showOnAllScreens
1089 | };
1090 |
1091 | string jsonString = JsonSerializer.Serialize(config, new JsonSerializerOptions
1092 | {
1093 | WriteIndented = true
1094 | });
1095 |
1096 | var configDir = Path.GetDirectoryName(configPath);
1097 | if (!Directory.Exists(configDir))
1098 | {
1099 | Directory.CreateDirectory(configDir);
1100 | }
1101 |
1102 | File.WriteAllText(configPath, jsonString);
1103 | }
1104 | catch (Exception ex)
1105 | {
1106 | MessageBox.Show($"保存横线配置时出错: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
1107 | }
1108 | }
1109 | }
1110 | }
--------------------------------------------------------------------------------
/Line/BoundingBoxForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using System.Drawing.Drawing2D;
4 | using System.Windows.Forms;
5 | using System.Runtime.InteropServices;
6 | using System.Collections.Generic;
7 | using System.IO;
8 | using System.Text.Json;
9 |
10 | namespace Line
11 | {
12 | // 可拖拽的包围框窗体
13 | public class DraggableBoundingBox : Form
14 | {
15 | private bool isDragging = false;
16 | private Point lastCursor;
17 | private bool mouseClickThrough;
18 | private DashStyle dashStyle;
19 | private Color lineColor;
20 | private int lineThickness;
21 | private Rectangle boundingRect;
22 | private Action onBoundsChanged; // 添加边界变化回调
23 |
24 | // 拖拽模式枚举
25 | private enum DragMode
26 | {
27 | None,
28 | Move, // 移动整个框
29 | ResizeTop, // 调整顶边
30 | ResizeBottom, // 调整底边
31 | ResizeLeft, // 调整左边
32 | ResizeRight, // 调整右边
33 | ResizeTopLeft, // 调整左上角
34 | ResizeTopRight, // 调整右上角
35 | ResizeBottomLeft, // 调整左下角
36 | ResizeBottomRight // 调整右下角
37 | }
38 |
39 | private DragMode currentDragMode = DragMode.None;
40 | private Rectangle initialBounds;
41 |
42 | // Windows API for mouse click-through
43 | private const int WS_EX_TRANSPARENT = 0x20;
44 | private const int WS_EX_LAYERED = 0x80000;
45 | private const int WS_EX_NOACTIVATE = 0x08000000;
46 | private const int GWL_EXSTYLE = (-20);
47 |
48 | // 窗口消息常量
49 | private const int WM_SETCURSOR = 0x0020;
50 | private const int WM_MOUSEACTIVATE = 0x0021;
51 | private const int MA_NOACTIVATE = 3;
52 |
53 | [DllImport("user32.dll")]
54 | private static extern int GetWindowLong(IntPtr hwnd, int index);
55 |
56 | [DllImport("user32.dll")]
57 | private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
58 |
59 | public DraggableBoundingBox(Rectangle bounds, Color color, int thickness, double opacity, bool clickThrough, DashStyle dashStyle, Action onBoundsChanged = null)
60 | {
61 | this.FormBorderStyle = FormBorderStyle.None;
62 | this.ShowInTaskbar = false;
63 | this.TopMost = true;
64 | this.BackColor = Color.Black;
65 | this.TransparencyKey = Color.Black;
66 | this.Opacity = opacity;
67 | this.AutoScaleMode = AutoScaleMode.None;
68 | this.StartPosition = FormStartPosition.Manual;
69 | this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true);
70 |
71 | // 设置窗体大小和位置
72 | this.Location = bounds.Location;
73 | this.Size = bounds.Size;
74 |
75 | mouseClickThrough = clickThrough;
76 | this.dashStyle = dashStyle;
77 | this.lineColor = color;
78 | this.lineThickness = thickness;
79 | this.boundingRect = new Rectangle(0, 0, bounds.Width, bounds.Height);
80 | this.onBoundsChanged = onBoundsChanged;
81 |
82 | // 如果不是鼠标穿透模式,添加拖拽事件
83 | if (!mouseClickThrough)
84 | {
85 | this.MouseDown += OnMouseDown;
86 | this.MouseMove += OnMouseMove;
87 | this.MouseUp += OnMouseUp;
88 | this.MouseLeave += OnMouseLeave;
89 | }
90 | }
91 |
92 | // 重写CreateParams,在创建窗口时就设置所有必要的扩展样式
93 | protected override CreateParams CreateParams
94 | {
95 | get
96 | {
97 | var cp = base.CreateParams;
98 | if (mouseClickThrough)
99 | {
100 | cp.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
101 | }
102 | else
103 | {
104 | cp.ExStyle |= WS_EX_LAYERED | WS_EX_NOACTIVATE;
105 | }
106 | return cp;
107 | }
108 | }
109 |
110 | protected override void OnHandleCreated(EventArgs e)
111 | {
112 | base.OnHandleCreated(e);
113 | if (mouseClickThrough)
114 | {
115 | int ex = GetWindowLong(this.Handle, GWL_EXSTYLE);
116 | ex |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
117 | SetWindowLong(this.Handle, GWL_EXSTYLE, ex);
118 | }
119 | }
120 |
121 | protected override void OnPaint(PaintEventArgs e)
122 | {
123 | base.OnPaint(e);
124 |
125 | using (Pen pen = new Pen(lineColor, lineThickness))
126 | {
127 | pen.DashStyle = dashStyle;
128 |
129 | // 绘制矩形框,稍微向内缩进以确保线条完全可见
130 | int halfThickness = lineThickness / 2;
131 | Rectangle drawRect = new Rectangle(
132 | halfThickness,
133 | halfThickness,
134 | this.Width - lineThickness,
135 | this.Height - lineThickness
136 | );
137 |
138 | e.Graphics.DrawRectangle(pen, drawRect);
139 | }
140 | }
141 |
142 | // 根据鼠标位置确定拖拽模式和光标
143 | private DragMode GetDragModeFromPoint(Point point)
144 | {
145 | const int borderWidth = 10; // 边界检测宽度
146 |
147 | bool nearLeft = point.X <= borderWidth;
148 | bool nearRight = point.X >= this.Width - borderWidth;
149 | bool nearTop = point.Y <= borderWidth;
150 | bool nearBottom = point.Y >= this.Height - borderWidth;
151 |
152 | // 边角优先
153 | if (nearTop && nearLeft) return DragMode.ResizeTopLeft;
154 | if (nearTop && nearRight) return DragMode.ResizeTopRight;
155 | if (nearBottom && nearLeft) return DragMode.ResizeBottomLeft;
156 | if (nearBottom && nearRight) return DragMode.ResizeBottomRight;
157 |
158 | // 边线
159 | if (nearTop) return DragMode.ResizeTop;
160 | if (nearBottom) return DragMode.ResizeBottom;
161 | if (nearLeft) return DragMode.ResizeLeft;
162 | if (nearRight) return DragMode.ResizeRight;
163 |
164 | // 内部区域
165 | return DragMode.Move;
166 | }
167 |
168 | // 根据拖拽模式设置光标
169 | private void SetCursorForDragMode(DragMode mode)
170 | {
171 | switch (mode)
172 | {
173 | case DragMode.Move:
174 | this.Cursor = Cursors.SizeAll;
175 | break;
176 | case DragMode.ResizeTop:
177 | case DragMode.ResizeBottom:
178 | this.Cursor = Cursors.SizeNS;
179 | break;
180 | case DragMode.ResizeLeft:
181 | case DragMode.ResizeRight:
182 | this.Cursor = Cursors.SizeWE;
183 | break;
184 | case DragMode.ResizeTopLeft:
185 | case DragMode.ResizeBottomRight:
186 | this.Cursor = Cursors.SizeNWSE;
187 | break;
188 | case DragMode.ResizeTopRight:
189 | case DragMode.ResizeBottomLeft:
190 | this.Cursor = Cursors.SizeNESW;
191 | break;
192 | default:
193 | this.Cursor = Cursors.Default;
194 | break;
195 | }
196 | }
197 |
198 | public void SetClickThrough(bool enable)
199 | {
200 | mouseClickThrough = enable;
201 | if (this.Handle != IntPtr.Zero)
202 | {
203 | if (enable)
204 | {
205 | this.MouseDown -= OnMouseDown;
206 | this.MouseMove -= OnMouseMove;
207 | this.MouseUp -= OnMouseUp;
208 | this.MouseLeave -= OnMouseLeave;
209 | this.Cursor = Cursors.Default;
210 |
211 | int exStyle = GetWindowLong(this.Handle, GWL_EXSTYLE);
212 | SetWindowLong(this.Handle, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT);
213 | }
214 | else
215 | {
216 | int exStyle = GetWindowLong(this.Handle, GWL_EXSTYLE);
217 | SetWindowLong(this.Handle, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
218 |
219 | this.MouseDown -= OnMouseDown;
220 | this.MouseMove -= OnMouseMove;
221 | this.MouseUp -= OnMouseUp;
222 | this.MouseLeave -= OnMouseLeave;
223 | this.MouseDown += OnMouseDown;
224 | this.MouseMove += OnMouseMove;
225 | this.MouseUp += OnMouseUp;
226 | this.MouseLeave += OnMouseLeave;
227 | }
228 | }
229 | }
230 |
231 | public void SetDashStyle(DashStyle style)
232 | {
233 | dashStyle = style;
234 | this.Invalidate();
235 | }
236 |
237 | public void SetLineColor(Color color)
238 | {
239 | this.lineColor = color;
240 | this.Invalidate();
241 | }
242 |
243 | public void SetLineThickness(int thickness)
244 | {
245 | this.lineThickness = thickness;
246 | this.Invalidate();
247 | }
248 |
249 | // 拦截窗口消息,解决光标和激活问题
250 | protected override void WndProc(ref Message m)
251 | {
252 | if (mouseClickThrough)
253 | {
254 | if (m.Msg == WM_MOUSEACTIVATE)
255 | {
256 | m.Result = new IntPtr(MA_NOACTIVATE);
257 | return;
258 | }
259 | if (m.Msg == WM_SETCURSOR)
260 | {
261 | return;
262 | }
263 | }
264 | base.WndProc(ref m);
265 | }
266 |
267 | private void OnMouseDown(object sender, MouseEventArgs e)
268 | {
269 | if (e.Button == MouseButtons.Left)
270 | {
271 | isDragging = true;
272 | lastCursor = Cursor.Position;
273 | currentDragMode = GetDragModeFromPoint(e.Location);
274 | initialBounds = this.Bounds;
275 | }
276 | }
277 |
278 | private void OnMouseMove(object sender, MouseEventArgs e)
279 | {
280 | if (!isDragging)
281 | {
282 | // 更新光标样式
283 | DragMode mode = GetDragModeFromPoint(e.Location);
284 | SetCursorForDragMode(mode);
285 | return;
286 | }
287 |
288 | Point currentCursor = Cursor.Position;
289 | int deltaX = currentCursor.X - lastCursor.X;
290 | int deltaY = currentCursor.Y - lastCursor.Y;
291 |
292 | Rectangle newBounds = this.Bounds;
293 |
294 | switch (currentDragMode)
295 | {
296 | case DragMode.Move:
297 | newBounds.X += deltaX;
298 | newBounds.Y += deltaY;
299 | break;
300 |
301 | case DragMode.ResizeTop:
302 | newBounds.Y += deltaY;
303 | newBounds.Height -= deltaY;
304 | break;
305 |
306 | case DragMode.ResizeBottom:
307 | newBounds.Height += deltaY;
308 | break;
309 |
310 | case DragMode.ResizeLeft:
311 | newBounds.X += deltaX;
312 | newBounds.Width -= deltaX;
313 | break;
314 |
315 | case DragMode.ResizeRight:
316 | newBounds.Width += deltaX;
317 | break;
318 |
319 | case DragMode.ResizeTopLeft:
320 | newBounds.X += deltaX;
321 | newBounds.Y += deltaY;
322 | newBounds.Width -= deltaX;
323 | newBounds.Height -= deltaY;
324 | break;
325 |
326 | case DragMode.ResizeTopRight:
327 | newBounds.Y += deltaY;
328 | newBounds.Width += deltaX;
329 | newBounds.Height -= deltaY;
330 | break;
331 |
332 | case DragMode.ResizeBottomLeft:
333 | newBounds.X += deltaX;
334 | newBounds.Width -= deltaX;
335 | newBounds.Height += deltaY;
336 | break;
337 |
338 | case DragMode.ResizeBottomRight:
339 | newBounds.Width += deltaX;
340 | newBounds.Height += deltaY;
341 | break;
342 | }
343 |
344 | // 确保最小尺寸
345 | if (newBounds.Width < 50) newBounds.Width = 50;
346 | if (newBounds.Height < 50) newBounds.Height = 50;
347 |
348 | this.Bounds = newBounds;
349 | lastCursor = currentCursor;
350 |
351 | // 触发边界变化回调
352 | onBoundsChanged?.Invoke();
353 | }
354 |
355 | private void OnMouseUp(object sender, MouseEventArgs e)
356 | {
357 | if (e.Button == MouseButtons.Left)
358 | {
359 | isDragging = false;
360 | currentDragMode = DragMode.None;
361 | }
362 | }
363 |
364 | private void OnMouseLeave(object sender, EventArgs e)
365 | {
366 | if (!isDragging)
367 | {
368 | this.Cursor = Cursors.Default;
369 | }
370 | }
371 | }
372 |
373 | public class BoundingBoxForm : Form
374 | {
375 | private NotifyIcon trayIcon;
376 | private DraggableBoundingBox boundingBox;
377 | private bool isVisible = false;
378 |
379 | // 辅助线
380 | private Form topGuide, bottomGuide, leftGuide, rightGuide;
381 | private bool guideLinesEnabled = true; // 默认开启辅助线
382 |
383 | // 线条默认粗细为2像素
384 | private int lineThickness = 2;
385 |
386 | // 线条颜色,默认为红色
387 | private Color lineColor = Color.Red;
388 |
389 | // 线条透明度,默认为100%
390 | private int lineOpacity = 100;
391 |
392 | // 鼠标穿透设置,默认不穿透
393 | private bool mouseClickThrough = false;
394 |
395 | // 虚线样式,默认为实线
396 | private DashStyle dashStyle = DashStyle.Solid;
397 |
398 | // 默认矩形大小和位置 - 改为屏幕大约一半的大小
399 | private Rectangle defaultBounds;
400 |
401 | // 配置文件路径
402 | private readonly string configPath = Path.Combine(
403 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
404 | "ScreenLine",
405 | "boundingbox_config.json"
406 | );
407 |
408 | // 配置类
409 | private class Config
410 | {
411 | public int LineThickness { get; set; }
412 | public string LineColor { get; set; }
413 | public int LineOpacity { get; set; }
414 | public bool MouseClickThrough { get; set; }
415 | public int DashStyle { get; set; }
416 | public Rectangle DefaultBounds { get; set; }
417 | public bool GuideLinesEnabled { get; set; } // 添加辅助线开关配置
418 | }
419 |
420 | // 置顶相关API
421 | [DllImport("user32.dll")]
422 | private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
423 |
424 | [DllImport("user32.dll")]
425 | private static extern bool BringWindowToTop(IntPtr hWnd);
426 |
427 | private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
428 | private const uint SWP_NOMOVE = 0x0002;
429 | private const uint SWP_NOSIZE = 0x0001;
430 | private const uint SWP_SHOWWINDOW = 0x0040;
431 |
432 | public BoundingBoxForm(NotifyIcon existingTrayIcon)
433 | {
434 | this.AutoScaleMode = AutoScaleMode.None;
435 | this.trayIcon = existingTrayIcon;
436 |
437 | // 首先设置默认边界
438 | SetDefaultBounds();
439 |
440 | // 加载配置
441 | LoadConfig();
442 |
443 | InitializeComponent();
444 | AddBoundingBoxMenuItems();
445 | }
446 |
447 | private void InitializeComponent()
448 | {
449 | this.FormBorderStyle = FormBorderStyle.None;
450 | this.ShowInTaskbar = false;
451 | this.TopMost = true;
452 | this.AutoScaleMode = AutoScaleMode.None;
453 | this.Opacity = 0;
454 | this.Width = 1;
455 | this.Height = 1;
456 | }
457 |
458 | private void AddBoundingBoxMenuItems()
459 | {
460 | if (trayIcon?.ContextMenuStrip == null) return;
461 |
462 | ToolStripMenuItem boundingBoxMenu = new ToolStripMenuItem("包围框");
463 |
464 | // 显示/隐藏包围框
465 | ToolStripMenuItem toggleVisibilityItem = new ToolStripMenuItem("显示包围框", null, (s, e) => {
466 | ToggleBoundingBox();
467 | });
468 | toggleVisibilityItem.Checked = isVisible;
469 |
470 | // 辅助线开关
471 | var guideLineItem = new ToolStripMenuItem("显示辅助线", null, (s, e) => {
472 | ToggleGuideLines();
473 | if (s is ToolStripMenuItem menuItem)
474 | {
475 | menuItem.Checked = guideLinesEnabled;
476 | }
477 | });
478 | guideLineItem.Checked = guideLinesEnabled;
479 |
480 | // 鼠标穿透选项
481 | var mousePenetrationItem = new ToolStripMenuItem("鼠标穿透", null, (s, e) => {
482 | SetClickThrough(!mouseClickThrough);
483 | if (s is ToolStripMenuItem menuItem)
484 | {
485 | menuItem.Checked = mouseClickThrough;
486 | }
487 | });
488 | mousePenetrationItem.Checked = mouseClickThrough;
489 |
490 | // 线条粗细菜单
491 | ToolStripMenuItem lineThicknessItem = new ToolStripMenuItem("线条粗细");
492 | AddThicknessMenuItem(lineThicknessItem, "细线 (1像素)", 1);
493 | AddThicknessMenuItem(lineThicknessItem, "中等 (2像素)", 2);
494 | AddThicknessMenuItem(lineThicknessItem, "粗线 (3像素)", 3);
495 | AddThicknessMenuItem(lineThicknessItem, "很粗 (5像素)", 5);
496 |
497 | // 线条颜色菜单
498 | ToolStripMenuItem lineColorItem = new ToolStripMenuItem("线条颜色");
499 | AddColorMenuItem(lineColorItem, "红色", Color.Red);
500 | AddColorMenuItem(lineColorItem, "绿色", Color.Green);
501 | AddColorMenuItem(lineColorItem, "蓝色", Color.Blue);
502 | AddColorMenuItem(lineColorItem, "黄色", Color.Yellow);
503 | AddColorMenuItem(lineColorItem, "橙色", Color.Orange);
504 | AddColorMenuItem(lineColorItem, "紫色", Color.Purple);
505 | AddColorMenuItem(lineColorItem, "青色", Color.Cyan);
506 | AddColorMenuItem(lineColorItem, "黑色", Color.FromArgb(1, 1, 1));
507 | AddColorMenuItem(lineColorItem, "白色", Color.White);
508 |
509 | // 透明度菜单
510 | ToolStripMenuItem transparencyItem = new ToolStripMenuItem("线条透明度");
511 | AddTransparencyMenuItem(transparencyItem, "100% (不透明)", 100);
512 | AddTransparencyMenuItem(transparencyItem, "75%", 75);
513 | AddTransparencyMenuItem(transparencyItem, "50%", 50);
514 | AddTransparencyMenuItem(transparencyItem, "25%", 25);
515 |
516 | // 虚线样式菜单
517 | ToolStripMenuItem dashStyleItem = new ToolStripMenuItem("线条样式");
518 | AddDashStyleMenuItem(dashStyleItem, "实线", DashStyle.Solid);
519 | AddDashStyleMenuItem(dashStyleItem, "虚线", DashStyle.Dash);
520 | AddDashStyleMenuItem(dashStyleItem, "点线", DashStyle.Dot);
521 | AddDashStyleMenuItem(dashStyleItem, "点划线", DashStyle.DashDot);
522 | AddDashStyleMenuItem(dashStyleItem, "双点划线", DashStyle.DashDotDot);
523 |
524 | // 重置位置菜单
525 | ToolStripMenuItem resetPositionItem = new ToolStripMenuItem("重置位置", null, (s, e) => {
526 | ResetBoundingBox();
527 | });
528 |
529 | // 添加所有子菜单
530 | boundingBoxMenu.DropDownItems.Add(toggleVisibilityItem);
531 | boundingBoxMenu.DropDownItems.Add(guideLineItem);
532 | boundingBoxMenu.DropDownItems.Add(new ToolStripSeparator());
533 | boundingBoxMenu.DropDownItems.Add(mousePenetrationItem);
534 | boundingBoxMenu.DropDownItems.Add(lineThicknessItem);
535 | boundingBoxMenu.DropDownItems.Add(lineColorItem);
536 | boundingBoxMenu.DropDownItems.Add(transparencyItem);
537 | boundingBoxMenu.DropDownItems.Add(dashStyleItem);
538 | boundingBoxMenu.DropDownItems.Add(new ToolStripSeparator());
539 | boundingBoxMenu.DropDownItems.Add(resetPositionItem);
540 |
541 | // 在持续横线菜单之后插入包围框菜单
542 | int insertIndex = -1;
543 | for (int i = 0; i < trayIcon.ContextMenuStrip.Items.Count; i++)
544 | {
545 | if (trayIcon.ContextMenuStrip.Items[i] is ToolStripMenuItem menuItem &&
546 | menuItem.Text == "持续横线")
547 | {
548 | insertIndex = i + 1;
549 | break;
550 | }
551 | }
552 |
553 | if (insertIndex != -1)
554 | {
555 | trayIcon.ContextMenuStrip.Items.Insert(insertIndex, boundingBoxMenu);
556 | }
557 | else
558 | {
559 | // 如果找不到持续横线菜单,就在分隔符前插入
560 | int separatorIndex = -1;
561 | for (int i = 0; i < trayIcon.ContextMenuStrip.Items.Count; i++)
562 | {
563 | if (trayIcon.ContextMenuStrip.Items[i] is ToolStripSeparator)
564 | {
565 | separatorIndex = i;
566 | break;
567 | }
568 | }
569 |
570 | if (separatorIndex != -1)
571 | {
572 | trayIcon.ContextMenuStrip.Items.Insert(separatorIndex, boundingBoxMenu);
573 | }
574 | else
575 | {
576 | trayIcon.ContextMenuStrip.Items.Add(boundingBoxMenu);
577 | }
578 | }
579 | }
580 |
581 | private void AddThicknessMenuItem(ToolStripMenuItem parent, string text, int thickness)
582 | {
583 | var item = new ToolStripMenuItem(text, null, (s, e) => {
584 | ChangeLineThickness(thickness);
585 | });
586 | item.Checked = (thickness == lineThickness);
587 | parent.DropDownItems.Add(item);
588 | }
589 |
590 | private void AddColorMenuItem(ToolStripMenuItem parent, string name, Color color)
591 | {
592 | Bitmap colorPreview = new Bitmap(16, 16);
593 | using (Graphics g = Graphics.FromImage(colorPreview))
594 | {
595 | g.FillRectangle(new SolidBrush(color), 0, 0, 16, 16);
596 | g.DrawRectangle(Pens.Gray, 0, 0, 15, 15);
597 | }
598 |
599 | var item = new ToolStripMenuItem(name, colorPreview, (s, e) => {
600 | ChangeLineColor(color);
601 | });
602 | item.Checked = color.Equals(lineColor);
603 | parent.DropDownItems.Add(item);
604 | }
605 |
606 | private void AddTransparencyMenuItem(ToolStripMenuItem parent, string name, int value)
607 | {
608 | var item = new ToolStripMenuItem(name, null, (s, e) => {
609 | ChangeLineTransparency(value);
610 | });
611 | item.Checked = (value == lineOpacity);
612 | parent.DropDownItems.Add(item);
613 | }
614 |
615 | private void AddDashStyleMenuItem(ToolStripMenuItem parent, string name, DashStyle style)
616 | {
617 | var item = new ToolStripMenuItem(name, null, (s, e) => {
618 | ChangeDashStyle(style);
619 | });
620 | item.Checked = (style == dashStyle);
621 | parent.DropDownItems.Add(item);
622 | }
623 |
624 | private void ToggleBoundingBox()
625 | {
626 | if (isVisible)
627 | {
628 | HideBoundingBox();
629 | }
630 | else
631 | {
632 | ShowBoundingBox();
633 | }
634 | UpdateVisibilityMenuText();
635 | }
636 |
637 | private void ShowBoundingBox()
638 | {
639 | Screen currentScreen = Screen.FromPoint(Cursor.Position);
640 |
641 | // 如果已存在,先隐藏
642 | if (boundingBox != null) HideBoundingBox();
643 |
644 | // 计算包围框位置,确保在当前屏幕内
645 | Rectangle bounds = defaultBounds;
646 |
647 | // 如果默认位置超出当前屏幕,调整位置
648 | if (bounds.Right > currentScreen.Bounds.Right)
649 | bounds.X = currentScreen.Bounds.Right - bounds.Width;
650 | if (bounds.Bottom > currentScreen.Bounds.Bottom)
651 | bounds.Y = currentScreen.Bounds.Bottom - bounds.Height;
652 | if (bounds.X < currentScreen.Bounds.X)
653 | bounds.X = currentScreen.Bounds.X;
654 | if (bounds.Y < currentScreen.Bounds.Y)
655 | bounds.Y = currentScreen.Bounds.Y;
656 |
657 | // 确保最小大小
658 | if (bounds.Width < 100) bounds.Width = 100;
659 | if (bounds.Height < 100) bounds.Height = 100;
660 |
661 | // 创建包围框
662 | boundingBox = new DraggableBoundingBox(bounds, lineColor, lineThickness, lineOpacity / 100.0, mouseClickThrough, dashStyle, () => {
663 | UpdateGuideLines();
664 | });
665 | boundingBox.Show();
666 |
667 | // **关键:给操作系统一点时间把窗体"真正"摆到屏幕上**
668 | Application.DoEvents();
669 |
670 | // 创建辅助线 - 使用包围框的实际位置
671 | if (guideLinesEnabled)
672 | {
673 | CreateGuideLines(currentScreen, boundingBox.Bounds);
674 | }
675 |
676 | isVisible = true;
677 | }
678 |
679 | private void CreateGuideLines(Screen screen, Rectangle boundingRect)
680 | {
681 | // 清理现有辅助线
682 | HideGuideLines();
683 |
684 | // 计算辅助线的透明度(比主线条透明50%)
685 | double guideOpacity = (lineOpacity / 100.0) * 0.5;
686 | Color guideColor = lineColor;
687 |
688 | // 顶部辅助线 - 从包围框顶边延伸到屏幕两侧
689 | topGuide = CreateGuideLine(
690 | new Rectangle(screen.Bounds.X, boundingRect.Y, screen.Bounds.Width, lineThickness),
691 | guideColor, guideOpacity, DashStyle.Dash
692 | );
693 |
694 | // 底部辅助线 - 从包围框底边延伸到屏幕两侧
695 | bottomGuide = CreateGuideLine(
696 | new Rectangle(screen.Bounds.X, boundingRect.Bottom - lineThickness, screen.Bounds.Width, lineThickness),
697 | guideColor, guideOpacity, DashStyle.Dash
698 | );
699 |
700 | // 左侧辅助线 - 从包围框左边延伸到屏幕上下
701 | leftGuide = CreateGuideLine(
702 | new Rectangle(boundingRect.X, screen.Bounds.Y, lineThickness, screen.Bounds.Height),
703 | guideColor, guideOpacity, DashStyle.Dash
704 | );
705 |
706 | // 右侧辅助线 - 从包围框右边延伸到屏幕上下
707 | rightGuide = CreateGuideLine(
708 | new Rectangle(boundingRect.Right - lineThickness, screen.Bounds.Y, lineThickness, screen.Bounds.Height),
709 | guideColor, guideOpacity, DashStyle.Dash
710 | );
711 | }
712 |
713 | private Form CreateGuideLine(Rectangle bounds, Color color, double opacity, DashStyle style)
714 | {
715 | var guideLine = new GuideLineForm(bounds, color, opacity, style, lineThickness);
716 | guideLine.Show();
717 | return guideLine;
718 | }
719 |
720 | // 定义专门的辅助线窗体类
721 | private class GuideLineForm : Form
722 | {
723 | private Color lineColor;
724 | private DashStyle lineStyle;
725 | private int thickness;
726 |
727 | public GuideLineForm(Rectangle bounds, Color color, double opacity, DashStyle style, int thickness)
728 | {
729 | this.FormBorderStyle = FormBorderStyle.None;
730 | this.ShowInTaskbar = false;
731 | this.TopMost = true;
732 | this.BackColor = Color.Black;
733 | this.TransparencyKey = Color.Black;
734 | this.Opacity = opacity;
735 | this.Bounds = bounds;
736 | this.AutoScaleMode = AutoScaleMode.None;
737 | this.StartPosition = FormStartPosition.Manual;
738 |
739 | this.lineColor = color;
740 | this.lineStyle = style;
741 | this.thickness = thickness;
742 |
743 | this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
744 | }
745 |
746 | protected override CreateParams CreateParams
747 | {
748 | get
749 | {
750 | var cp = base.CreateParams;
751 | const int WS_EX_TRANSPARENT = 0x20;
752 | const int WS_EX_LAYERED = 0x80000;
753 | const int WS_EX_NOACTIVATE = 0x08000000;
754 | cp.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE;
755 | return cp;
756 | }
757 | }
758 |
759 | protected override void OnPaint(PaintEventArgs e)
760 | {
761 | base.OnPaint(e);
762 |
763 | using (Pen pen = new Pen(lineColor, thickness))
764 | {
765 | pen.DashStyle = lineStyle;
766 |
767 | if (this.Width > this.Height)
768 | {
769 | // 水平线
770 | int y = this.Height / 2;
771 | e.Graphics.DrawLine(pen, 0, y, this.Width, y);
772 | }
773 | else
774 | {
775 | // 垂直线
776 | int x = this.Width / 2;
777 | e.Graphics.DrawLine(pen, x, 0, x, this.Height);
778 | }
779 | }
780 | }
781 | }
782 |
783 | private void UpdateGuideLines()
784 | {
785 | if (!guideLinesEnabled || !isVisible || boundingBox == null)
786 | return;
787 |
788 | Screen currentScreen = Screen.FromPoint(boundingBox.Location);
789 | Rectangle boundingRect = boundingBox.Bounds;
790 |
791 | // 更新辅助线位置
792 | if (topGuide != null)
793 | {
794 | topGuide.Bounds = new Rectangle(currentScreen.Bounds.X, boundingRect.Y, currentScreen.Bounds.Width, lineThickness);
795 | topGuide.Invalidate();
796 | }
797 |
798 | if (bottomGuide != null)
799 | {
800 | bottomGuide.Bounds = new Rectangle(currentScreen.Bounds.X, boundingRect.Bottom - lineThickness, currentScreen.Bounds.Width, lineThickness);
801 | bottomGuide.Invalidate();
802 | }
803 |
804 | if (leftGuide != null)
805 | {
806 | leftGuide.Bounds = new Rectangle(boundingRect.X, currentScreen.Bounds.Y, lineThickness, currentScreen.Bounds.Height);
807 | leftGuide.Invalidate();
808 | }
809 |
810 | if (rightGuide != null)
811 | {
812 | rightGuide.Bounds = new Rectangle(boundingRect.Right - lineThickness, currentScreen.Bounds.Y, lineThickness, currentScreen.Bounds.Height);
813 | rightGuide.Invalidate();
814 | }
815 | }
816 |
817 | private void HideGuideLines()
818 | {
819 | if (topGuide != null) { topGuide.Close(); topGuide = null; }
820 | if (bottomGuide != null) { bottomGuide.Close(); bottomGuide = null; }
821 | if (leftGuide != null) { leftGuide.Close(); leftGuide = null; }
822 | if (rightGuide != null) { rightGuide.Close(); rightGuide = null; }
823 | }
824 |
825 | private void HideBoundingBox()
826 | {
827 | if (boundingBox != null) { boundingBox.Close(); boundingBox = null; }
828 | HideGuideLines();
829 |
830 | isVisible = false;
831 | }
832 |
833 | private void ResetBoundingBox()
834 | {
835 | Screen currentScreen = Screen.FromPoint(Cursor.Position);
836 |
837 | // 重置到当前屏幕中央,大约屏幕大小的一半
838 | int width = currentScreen.Bounds.Width / 2;
839 | int height = currentScreen.Bounds.Height / 2;
840 |
841 | defaultBounds = new Rectangle(
842 | currentScreen.Bounds.X + (currentScreen.Bounds.Width - width) / 2,
843 | currentScreen.Bounds.Y + (currentScreen.Bounds.Height - height) / 2,
844 | width,
845 | height
846 | );
847 |
848 | if (isVisible)
849 | {
850 | HideBoundingBox();
851 | ShowBoundingBox();
852 | }
853 |
854 | SaveConfig();
855 | }
856 |
857 | private void SetClickThrough(bool enable)
858 | {
859 | mouseClickThrough = enable;
860 |
861 | if (boundingBox != null) boundingBox.SetClickThrough(enable);
862 |
863 | SaveConfig();
864 | }
865 |
866 | private void ChangeLineThickness(int thickness)
867 | {
868 | lineThickness = thickness;
869 |
870 | if (boundingBox != null)
871 | {
872 | boundingBox.SetLineThickness(thickness);
873 | }
874 |
875 | UpdateThicknessMenuCheckedState();
876 | SaveConfig();
877 | }
878 |
879 | private void ChangeLineColor(Color color)
880 | {
881 | lineColor = color;
882 |
883 | if (boundingBox != null) boundingBox.SetLineColor(color);
884 |
885 | UpdateColorMenuCheckedState();
886 | SaveConfig();
887 | }
888 |
889 | private void ChangeLineTransparency(int value)
890 | {
891 | lineOpacity = value;
892 | double opacity = value / 100.0;
893 |
894 | if (boundingBox != null) boundingBox.Opacity = opacity;
895 |
896 | UpdateTransparencyMenuCheckedState();
897 | SaveConfig();
898 | }
899 |
900 | private void ChangeDashStyle(DashStyle style)
901 | {
902 | dashStyle = style;
903 |
904 | if (boundingBox != null) boundingBox.SetDashStyle(style);
905 |
906 | UpdateDashStyleMenuCheckedState();
907 | SaveConfig();
908 | }
909 |
910 | private Rectangle GetCurrentBounds()
911 | {
912 | if (boundingBox != null)
913 | {
914 | return boundingBox.Bounds;
915 | }
916 | return defaultBounds;
917 | }
918 |
919 | private void UpdateVisibilityMenuText()
920 | {
921 | UpdateMenuCheckedState("显示包围框", "隐藏包围框", isVisible);
922 | }
923 |
924 | private void UpdateThicknessMenuCheckedState()
925 | {
926 | UpdateMenuCheckedState("线条粗细", item => {
927 | string thicknessStr = lineThickness.ToString();
928 | return item.Text.Contains(thicknessStr);
929 | });
930 | }
931 |
932 | private void UpdateColorMenuCheckedState()
933 | {
934 | UpdateMenuCheckedState("线条颜色", item => {
935 | if (item.Image is Bitmap bmp)
936 | {
937 | try
938 | {
939 | Color menuColor = bmp.GetPixel(8, 8);
940 | return Math.Abs(menuColor.R - lineColor.R) < 5 &&
941 | Math.Abs(menuColor.G - lineColor.G) < 5 &&
942 | Math.Abs(menuColor.B - lineColor.B) < 5;
943 | }
944 | catch
945 | {
946 | return false;
947 | }
948 | }
949 | return false;
950 | });
951 | }
952 |
953 | private void UpdateTransparencyMenuCheckedState()
954 | {
955 | UpdateMenuCheckedState("线条透明度", item => item.Text.Contains(lineOpacity.ToString() + "%"));
956 | }
957 |
958 | private void UpdateDashStyleMenuCheckedState()
959 | {
960 | UpdateMenuCheckedState("线条样式", item => {
961 | return (item.Text == "实线" && dashStyle == DashStyle.Solid) ||
962 | (item.Text == "虚线" && dashStyle == DashStyle.Dash) ||
963 | (item.Text == "点线" && dashStyle == DashStyle.Dot) ||
964 | (item.Text == "点划线" && dashStyle == DashStyle.DashDot) ||
965 | (item.Text == "双点划线" && dashStyle == DashStyle.DashDotDot);
966 | });
967 | }
968 |
969 | private void UpdateMenuCheckedState(string menuName, Func checkCondition)
970 | {
971 | if (trayIcon?.ContextMenuStrip == null) return;
972 |
973 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
974 | {
975 | if (item is ToolStripMenuItem boundingBoxMenu && boundingBoxMenu.Text == "包围框")
976 | {
977 | foreach (ToolStripItem subItem in boundingBoxMenu.DropDownItems)
978 | {
979 | if (subItem is ToolStripMenuItem targetMenu && targetMenu.Text == menuName)
980 | {
981 | foreach (ToolStripItem optionItem in targetMenu.DropDownItems)
982 | {
983 | if (optionItem is ToolStripMenuItem menuItem)
984 | {
985 | menuItem.Checked = checkCondition(menuItem);
986 | }
987 | }
988 | break;
989 | }
990 | }
991 | break;
992 | }
993 | }
994 | }
995 |
996 | private void UpdateMenuCheckedState(string showText, string hideText, bool isChecked)
997 | {
998 | if (trayIcon?.ContextMenuStrip == null) return;
999 |
1000 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
1001 | {
1002 | if (item is ToolStripMenuItem boundingBoxMenu && boundingBoxMenu.Text == "包围框")
1003 | {
1004 | foreach (ToolStripItem subItem in boundingBoxMenu.DropDownItems)
1005 | {
1006 | if (subItem is ToolStripMenuItem menuItem &&
1007 | (menuItem.Text == showText || menuItem.Text == hideText))
1008 | {
1009 | menuItem.Text = isChecked ? hideText : showText;
1010 | menuItem.Checked = isChecked;
1011 | break;
1012 | }
1013 | }
1014 | break;
1015 | }
1016 | }
1017 | }
1018 |
1019 | ///
1020 | /// 确保所有包围线保持置顶状态
1021 | ///
1022 | public void EnsureTopmost()
1023 | {
1024 | if (boundingBox != null && !boundingBox.IsDisposed && boundingBox.Visible && boundingBox.Handle != IntPtr.Zero)
1025 | {
1026 | boundingBox.TopMost = false;
1027 | boundingBox.TopMost = true;
1028 |
1029 | SetWindowPos(boundingBox.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
1030 | BringWindowToTop(boundingBox.Handle);
1031 | }
1032 |
1033 | // 确保辅助线也保持置顶
1034 | var guides = new[] { topGuide, bottomGuide, leftGuide, rightGuide };
1035 | foreach (var guide in guides)
1036 | {
1037 | if (guide != null && !guide.IsDisposed && guide.Visible && guide.Handle != IntPtr.Zero)
1038 | {
1039 | guide.TopMost = false;
1040 | guide.TopMost = true;
1041 |
1042 | SetWindowPos(guide.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
1043 | BringWindowToTop(guide.Handle);
1044 | }
1045 | }
1046 | }
1047 |
1048 | ///
1049 | /// 显示所有包围线
1050 | ///
1051 | public void ShowAllLines()
1052 | {
1053 | if (isVisible && boundingBox != null && !boundingBox.Visible)
1054 | {
1055 | boundingBox.Show();
1056 | }
1057 |
1058 | if (guideLinesEnabled)
1059 | {
1060 | var guides = new[] { topGuide, bottomGuide, leftGuide, rightGuide };
1061 | foreach (var guide in guides)
1062 | {
1063 | if (guide != null && !guide.Visible)
1064 | {
1065 | guide.Show();
1066 | }
1067 | }
1068 | }
1069 | }
1070 |
1071 | ///
1072 | /// 隐藏所有包围线
1073 | ///
1074 | public void HideAllLines()
1075 | {
1076 | if (isVisible && boundingBox != null && boundingBox.Visible)
1077 | {
1078 | boundingBox.Hide();
1079 | }
1080 |
1081 | var guides = new[] { topGuide, bottomGuide, leftGuide, rightGuide };
1082 | foreach (var guide in guides)
1083 | {
1084 | if (guide != null && guide.Visible)
1085 | {
1086 | guide.Hide();
1087 | }
1088 | }
1089 | }
1090 |
1091 | ///
1092 | /// 关闭所有包围线
1093 | ///
1094 | public void CloseAllLines()
1095 | {
1096 | HideBoundingBox();
1097 | UpdateVisibilityMenuText();
1098 | }
1099 |
1100 | ///
1101 | /// 重新置顶所有包围线
1102 | ///
1103 | public void BringAllLinesToTop()
1104 | {
1105 | EnsureTopmost();
1106 | }
1107 |
1108 | protected override void OnFormClosing(FormClosingEventArgs e)
1109 | {
1110 | HideBoundingBox();
1111 | base.OnFormClosing(e);
1112 | }
1113 |
1114 | // 加载配置
1115 | private void LoadConfig()
1116 | {
1117 | try
1118 | {
1119 | if (File.Exists(configPath))
1120 | {
1121 | string jsonString = File.ReadAllText(configPath);
1122 | var config = JsonSerializer.Deserialize(jsonString);
1123 |
1124 | lineThickness = config.LineThickness;
1125 | lineColor = ColorTranslator.FromHtml(config.LineColor);
1126 | lineOpacity = config.LineOpacity;
1127 | mouseClickThrough = config.MouseClickThrough;
1128 | dashStyle = (DashStyle)config.DashStyle;
1129 | defaultBounds = config.DefaultBounds;
1130 | guideLinesEnabled = config.GuideLinesEnabled;
1131 | }
1132 | else
1133 | {
1134 | // 第一次运行,设置默认值
1135 | guideLinesEnabled = true; // 默认开启辅助线
1136 | SetDefaultBounds();
1137 | }
1138 | }
1139 | catch
1140 | {
1141 | // 如果加载失败,使用默认值
1142 | lineThickness = 2;
1143 | lineColor = Color.Red;
1144 | lineOpacity = 100;
1145 | mouseClickThrough = false;
1146 | dashStyle = DashStyle.Solid;
1147 | guideLinesEnabled = true; // 默认开启辅助线
1148 |
1149 | SetDefaultBounds();
1150 | }
1151 | }
1152 |
1153 | private void SetDefaultBounds()
1154 | {
1155 | Screen primaryScreen = Screen.PrimaryScreen;
1156 | int width = primaryScreen.Bounds.Width / 2; // 屏幕宽度的一半
1157 | int height = primaryScreen.Bounds.Height / 2; // 屏幕高度的一半
1158 |
1159 | defaultBounds = new Rectangle(
1160 | primaryScreen.Bounds.X + (primaryScreen.Bounds.Width - width) / 2,
1161 | primaryScreen.Bounds.Y + (primaryScreen.Bounds.Height - height) / 2,
1162 | width,
1163 | height
1164 | );
1165 | }
1166 |
1167 | // 保存配置
1168 | private void SaveConfig()
1169 | {
1170 | try
1171 | {
1172 | // 如果包围框可见,更新默认位置
1173 | if (isVisible)
1174 | {
1175 | defaultBounds = GetCurrentBounds();
1176 | }
1177 |
1178 | var config = new Config
1179 | {
1180 | LineThickness = lineThickness,
1181 | LineColor = ColorTranslator.ToHtml(lineColor),
1182 | LineOpacity = lineOpacity,
1183 | MouseClickThrough = mouseClickThrough,
1184 | DashStyle = (int)dashStyle,
1185 | DefaultBounds = defaultBounds,
1186 | GuideLinesEnabled = guideLinesEnabled
1187 | };
1188 |
1189 | string jsonString = JsonSerializer.Serialize(config, new JsonSerializerOptions
1190 | {
1191 | WriteIndented = true
1192 | });
1193 |
1194 | var configDir = Path.GetDirectoryName(configPath);
1195 | if (!Directory.Exists(configDir))
1196 | {
1197 | Directory.CreateDirectory(configDir);
1198 | }
1199 |
1200 | File.WriteAllText(configPath, jsonString);
1201 | }
1202 | catch
1203 | {
1204 | // 忽略保存错误
1205 | }
1206 | }
1207 |
1208 | private void ToggleGuideLines()
1209 | {
1210 | guideLinesEnabled = !guideLinesEnabled;
1211 |
1212 | if (isVisible)
1213 | {
1214 | if (guideLinesEnabled)
1215 | {
1216 | // 重新创建辅助线
1217 | if (boundingBox != null)
1218 | {
1219 | Screen currentScreen = Screen.FromPoint(boundingBox.Location);
1220 | CreateGuideLines(currentScreen, boundingBox.Bounds);
1221 | }
1222 | }
1223 | else
1224 | {
1225 | // 隐藏辅助线
1226 | HideGuideLines();
1227 | }
1228 | }
1229 |
1230 | UpdateGuideLinesMenuCheckedState();
1231 | SaveConfig();
1232 | }
1233 |
1234 | private void UpdateGuideLinesMenuCheckedState()
1235 | {
1236 | if (trayIcon?.ContextMenuStrip == null) return;
1237 |
1238 | foreach (ToolStripItem item in trayIcon.ContextMenuStrip.Items)
1239 | {
1240 | if (item is ToolStripMenuItem boundingBoxMenu && boundingBoxMenu.Text == "包围框")
1241 | {
1242 | foreach (ToolStripItem subItem in boundingBoxMenu.DropDownItems)
1243 | {
1244 | if (subItem is ToolStripMenuItem menuItem && menuItem.Text == "显示辅助线")
1245 | {
1246 | menuItem.Checked = guideLinesEnabled;
1247 | break;
1248 | }
1249 | }
1250 | break;
1251 | }
1252 | }
1253 | }
1254 | }
1255 | }
--------------------------------------------------------------------------------