27寸屏左移后,这个小工具救了我无数次
27 寸屏放左边爆发新问题,177 行换回一个能点到的"显示桌面"
显示器换成 27 寸并挪到左侧当主屏之后,Windows 任务栏最右侧那条"显示桌面"细条跟着遭了殃:它固定在所有屏幕拼接后的最右端,如今落在笔记本屏的边缘。鼠标从 27 寸主屏划过去,快一点就冲出细条、甩到屏幕外。于是有了这个工具——一个托盘程序,点一下图标等于按一次 Win+D。5 个 C# 文件共 177 行,编译出来一个 48MB 的单文件 exe,代码全部贴在下面。
一、细线写死在最右端,主屏偏偏在左
这条细线的位置在系统里没有配置项:永远在任务栏最右端,也就是所有显示器拼接结果的右边缘。显示器在笔记本右侧时,细线正好挨着主屏,好点。把 27 寸设为主屏并放到左边之后,最右端属于笔记本屏,点击路径变成——从 27 寸出发,横穿整台笔记本,最后停在右边缘一条细线上。划快了指针直接出界。

上图中间那条竖线就是它。搬显示器之前它一直在那,搬完之后这个动作一天要做很多次,每次都费劲。
二、切换逻辑只有一个方法
不枚举窗口,不记录窗口状态。Shell 自带 COM 接口 IShellDispatch2.ToggleDesktop,行为与 Win+D 完全一致:第一次调用显示桌面,再次调用恢复现场。ShellToggle.cs 全文 30 行:
using System;
namespace ShowDesktopTrayLite;
/// <summary>
/// Toggles the desktop exactly like pressing Win + D: first call shows the
/// desktop (minimizes all windows), second call restores them.
/// Implemented via the Shell's ToggleDesktop method (IShellDispatch2).
/// </summary>
internal static class ShellToggle
{
public static void ToggleDesktop()
{
Type? shellType = Type.GetTypeFromProgID("Shell.Application");
if (shellType is null)
return;
// Must be called on an STA thread (WinForms UI thread is STA).
dynamic shell = Activator.CreateInstance(shellType)!;
try
{
shell.ToggleDesktop();
}
finally
{
if (shell is IDisposable disposable)
disposable.Dispose();
}
}
}
两个实现细节。ProgID "Shell.Application" 在运行时解析,不需要引用任何 COM 互操作程序集;调用必须在 STA 线程上,WinForms 的 UI 线程天然满足。没有用 SendInput 模拟 Win+D——模拟按键要经过前台窗口这一层,全屏程序、焦点切换都可能把按键吞掉;COM 调用直达 Shell,中间没有变量。
Shell 其实还提供 MinimizeAll 和 UndoMinimizeALL,但那是两个独立动作,"现在该最小化还是恢复"的判断要自己写;ToggleDesktop 自带切换语义,一次调用解决。
三、托盘:不建窗口,只放图标
托盘部分合计 78 行(TrayApplicationContext 58 行 + TrayIconFactory 20 行)。主程序用 ApplicationContext 而不是 Form:不建窗口,就没有窗口会被 Win+D 顺带最小化,任务栏上也不会多出一个程序图标,托盘里那个电脑图标就是它的全部。
using System.Windows.Forms;
namespace ShowDesktopTrayLite;
/// <summary>
/// Hosts only a notify (tray) icon — no visible window is ever created.
/// Left click toggles show-desktop, exactly like pressing Win + D.
/// </summary>
public sealed class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _notifyIcon;
public TrayApplicationContext()
{
_notifyIcon = new NotifyIcon
{
Icon = TrayIconFactory.Create(),
Text = "显示桌面 / 恢复窗口 (Win+D)",
Visible = true
};
var menu = new ContextMenuStrip();
var toggle = new ToolStripMenuItem("切换 (显示桌面 / 恢复)", null, (_, _) => ShellToggle.ToggleDesktop());
var autoStart = new ToolStripMenuItem("开机自启动");
autoStart.Checked = AutoStart.IsEnabled;
autoStart.Click += (_, _) => autoStart.Checked = AutoStart.Toggle();
var exit = new ToolStripMenuItem("退出", null, (_, _) => Exit());
menu.Items.Add(toggle);
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add(autoStart);
menu.Items.Add(exit);
_notifyIcon.ContextMenuStrip = menu;
_notifyIcon.MouseClick += (_, e) =>
{
if (e.Button == MouseButtons.Left)
ShellToggle.ToggleDesktop();
};
}
private void Exit()
{
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
ExitThread();
}
protected override void Dispose(bool disposing)
{
if (disposing)
_notifyIcon?.Dispose();
base.Dispose(disposing);
}
}
左键 MouseClick 触发切换;右键是菜单,三项:切换、开机自启动、退出。
图标没去找 .ico 文件,PNG 直接作为嵌入资源编进 exe——32x32、825 字节,运行时用 GetHicon() 转成带透明通道的 Icon:
using System.Drawing;
namespace ShowDesktopTrayLite;
internal static class TrayIconFactory
{
public static Icon Create()
{
var assembly = typeof(TrayIconFactory).Assembly;
var resourceName = $"{assembly.GetName().Name}.029-computer.png";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded resource '{resourceName}' not found.");
using var bitmap = new Bitmap(stream);
return Icon.FromHandle(bitmap.GetHicon());
}
}
嵌入资源的名字规则是"程序集名.文件名",csproj 里一行 <EmbeddedResource Include="029-computer.png" /> 搞定。
四、开机自启:一个注册表键,零管理员权限
自启动开关写的是 HKCU\Software\Microsoft\Windows\CurrentVersion\Run——当前用户的键,不碰任务计划程序,全程不需要管理员权限。AutoStart.cs 全文 43 行:
using Microsoft.Win32;
namespace ShowDesktopTrayLite;
/// <summary>
/// Persists the "run at logon" preference in the per-user registry Run key
/// (HKCU\Software\Microsoft\Windows\CurrentVersion\Run). No admin rights needed.
/// </summary>
internal static class AutoStart
{
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string ValueName = "ShowDesktopTrayLite";
public static bool IsEnabled
{
get
{
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath);
return key?.GetValue(ValueName) is string;
}
}
public static bool Toggle()
{
if (IsEnabled)
Disable();
else
Enable();
return IsEnabled;
}
public static void Enable()
{
using var key = Registry.CurrentUser.CreateSubKey(RunKeyPath);
key.SetValue(ValueName, Environment.ProcessPath ?? string.Empty, RegistryValueKind.String);
}
public static void Disable()
{
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, writable: true);
key?.DeleteValue(ValueName, throwOnMissingValue: false);
}
}
Environment.ProcessPath 拿到当前 exe 的完整路径。单文件发布下它指向的就是那一个文件,拷到哪个目录,注册表里就写哪个目录。单实例靠一个本地互斥体,Program.cs 全文 26 行:
using System.Threading;
using System.Windows.Forms;
namespace ShowDesktopTrayLite;
internal static class Program
{
// Per-session mutex: prevents multiple tray icons in the same session.
private const string MutexName = "Local\\ShowDesktopTrayLite";
[STAThread]
static void Main()
{
using var singleInstance = new Mutex(true, MutexName, out bool created);
if (!created)
{
// An instance is already running in this session.
return;
}
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TrayApplicationContext());
}
}
五、发布:一条命令,一个文件
发布配置固化在 csproj 里(28 行),之后只剩一条命令 dotnet publish -c Release:
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<DebugType>embedded</DebugType>
publish 目录里只有一个 ShowDesktopTrayLite.exe,48MB。体积来自自包含的 .NET 9 运行时——整个运行时压进了这一个文件,换来目标机器零依赖。嫌 48MB 大,把 SelfContained 改成 false,exe 缩到 1MB 以内,代价是目标机要装 .NET 桌面运行时。托盘这种小工具我选择背运行时,拷给谁都能双击。
这个工具解决的不是系统缺陷,是布局冲突:细线位置写死在拼接桌面的右端,显示器挪到左边,冲突就出现了。哪天微软把"显示桌面"按钮的位置做成可配置,这 177 行可以立刻删掉;在那之前,它先在托盘里待着。要加功能的话,下一步是把中键做成第二个动作,比如最小化全部窗口。
运行展示

点击图标后可以隐藏/显示桌面,右键功能:


浙公网安备 33010602011771号