将Windows窗体设置为最底层

17

背景

我的一个客户经营着一家互联网点,客户可以通过设置为“亭”的PC访问网络(一种自定义应用程序会“锁定”计算机,直到用户登录,而运行的帐户受到Windows组策略的严格限制)。目前,每台电脑都运行Windows XP,并使用Active Desktop在后台显示广告。然而,由于我的客户经常遇到Active Desktop崩溃的问题(除了一般减慢计算机),我被要求开发一款替代它的应用程序。

问题

我正在尝试调查是否可能构建一个 Windows 窗体应用程序(使用 C#),它 始终保持在后台。该应用程序应该位于桌面上方(以便覆盖任何图标、文件等),但始终在所有其他正在运行的应用程序之后。我想我真的在寻找Form类的 BottomMost 属性(当然,这不存在)。

如果您对如何实现此目标有任何提示或指针,将不胜感激。


2
请记住,当用户按下Win+M或Win+D时,您的窗口可能会被隐藏。另外,用几乎什么都不做的自定义 shell 替换 Explorer 不是更容易吗?毕竟,在 kiosk 环境中,您不需要任务栏、进程切换、桌面图标等功能。 - Joey
实际上,用户仍然可以通过开始菜单启动程序、在它们之间切换等操作,因此我认为在这种特定情况下完全替换资源管理器可能有些过度。不过我理解你的观点。 - Anders Fjeldstad
啊,好的,那就忽略我说的一半吧 : ) 。不过,Win+D 和 Win+M 的问题可能还会存在(以及右键单击任务栏并选择“最小化所有窗口”)。 - Joey
没错,这是一个有价值的观点,也是需要解决的另一个问题。(实际上,我几乎希望能够用一个好的解释来拒绝整个任务,并向客户说明各种困难和不完美的解决方案,尽管它可以满足他们的需求...) - Anders Fjeldstad
我一直知道这个问题会出现在这里,哈哈。谢谢你的提问。 - jay_t55
7个回答

21

这不是直接由.NET Form类支持的,所以你有两个选项:

1)使用Win32 API SetWindowPos函数。

pinvoke.net展示了如何在C#中声明使用该函数:

[DllImport("user32.dll")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
const UInt32 SWP_NOSIZE = 0x0001;
const UInt32 SWP_NOMOVE = 0x0002;
const UInt32 SWP_NOACTIVATE = 0x0010;

因此在您的代码中,调用:

SetWindowPos(Handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

正如你所评论的那样,这会将表单移动到z-order的底部,但不会使其保持在那里。 我能想到的唯一解决方法是从Form_LoadForm_Activate事件调用SetWindowPos。 如果您的应用程序已最大化且用户无法移动或最小化表单,则可能可以使用此方法解决问题,但仍然有点像一个hack。此外,如果在进行SetWindowPos调用之前,将表单提到z-order的前面,用户可能会看到轻微的“闪烁”。


2) 子类化表单,覆盖WndProc函数,并拦截WM_WINDOWPOSCHANGING Windows消息,设置SWP_NOZORDER标志(取自此页面)。


Richard,感谢您的回答。据我所知,SetWindowPos允许我将窗体发送到后面,但是每当它获得焦点时,它就会被推到前面。即使用户正在与其交互,我也希望它保持在后面。 - Anders Fjeldstad
1
我将您的答案标记为已接受,因为它“足够接近”-从我目前找到的内容来看,我认为我无法以任何不太可靠的方式解决这个问题...感谢您的时间! - Anders Fjeldstad
我不认为子类化表单是一种hackish的方法,它只是有点棘手。 - Richard Ev
2
你说得对 - 我实际上忽略了选项2,我的评论是针对第一个选项的。第二个很好用。谢谢! - Anders Fjeldstad
对于其他的Electron开发者:我花了一些时间,但是我找到了如何在NodeJS/Electron中实现选项1和2的方法:https://dev59.com/7Jrga4cB1Zd3GeqPqKhq#58473299 - Venryx

4
我认为最好的方法是使用激活事件处理程序和SendToBack方法,像这样:

我认为最好的方法是使用激活事件处理程序和 SendToBack 方法,就像这样:

private void Form1_Activated(object sender, EventArgs e)
{
    this.SendToBack();
}

这是最直接的答案。谢谢! - Dombi Bence

3
将您的窗口设置为桌面的子窗口(“程序管理器”或“progman”进程)。我在Windows XP(x86)和Windows Vista(x64)中使用此方法成功。
当我搜索如何使屏幕保护程序显示为壁纸时,我偶然发现了这种方法。原来,这在系统的.scr处理程序中已经内置了。您可以使用“screensaver.scr /p PID”,其中PID是要附加到的另一个程序的进程ID。因此,编写一个程序查找progman的句柄,然后使用它作为/p参数调用.scr,您就可以获得屏幕保护壁纸!
我正在尝试制作桌面状态显示项目(显示时间、一些任务、安装的磁盘等),它基于Strawberry Perl和普通的Win32 APIS(主要是Win32 :: GUI和Win32 :: API模块),因此代码易于移植或理解任何具有类似Win32 API绑定或访问Windows Scripting Host(例如ActivePerl、Python、JScript、VBScript)的动态语言。以下是生成窗口的类的相关部分:
do { Win32::API->Import(@$_) or die "Win32::API can't import @$_ ($^E)" } for
    [user32 => 'HWND FindWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName)'],
    [user32 => 'HWND SetParent(HWND hWndChild, HWND hWndNewParent)'],

sub __screen_x {
    Win32::GUI::GetSystemMetrics(SM_CXSCREEN)
}
sub __screen_y {
    Win32::GUI::GetSystemMetrics(SM_CYSCREEN)
}
sub _create_window { # create window that covers desktop
    my $self = shift;
    my $wnd = $$self{_wnd} = Win32::GUI::Window->new(
        -width   => __screen_x(), -left => 0,
        -height  => __screen_y(), -top  => 0,
    ) or die "can't create window ($^E)";

    $wnd->SetWindowLong(GWL_STYLE,
        WS_VISIBLE
        | WS_POPUP # popup: no caption or border
    );
    $wnd->SetWindowLong(GWL_EXSTYLE, 
        WS_EX_NOACTIVATE # noactivate: doesn't activate when clicked
        | WS_EX_NOPARENTNOTIFY # noparentnotify: doesn't notify parent window when created or destroyed
        | WS_EX_TOOLWINDOW # toolwindow: hide from taskbar
    );
    SetParent($$wnd{-handle}, # pin window to desktop (bottommost)
        (FindWindow('Progman', 'Program Manager') or die "can't find desktop window ($^E)")
    ) or die "can't pin to desktop ($^E)";
    Win32::GUI::DoEvents; # allow sizing and styling to take effect (otherwise DC bitmaps are the wrong size)
}

这个程序缓冲输出以防止闪烁,你也可能想要这么做。我创建了一个DC(设备上下文)并将PaintDesktop绘制到它上面(你可以使用任何位图,只需要再添加几行代码-CreateCompatibleBitmap、读入文件并选择位图的句柄作为画刷),然后创建一个保持干净背景的缓冲区和一个工作缓冲区来组装这些部分——在每个循环中,先复制背景,然后绘制线条和画刷位图并使用TextOut——然后将其复制到原始DC上,在此时它就会出现在屏幕上。


在Windows 8中,托管桌面的过程是什么?我尝试了DWM,但我的窗口没有附加到它上面。 - bitdisaster
将窗口设置为桌面(progman)的子级非常有效。在C#中,相应的代码是 IntPtr DesktopWindow = FindWindow("Progman", "Program Manager"); 以获取指向桌面的指针,然后使用 if (DesktopWindow != IntPtr.Zero) { SetParent(hMyWindow, DesktopWindow); } 将其置于桌面顶部但任务栏下方。 - ingenuine

0

是的,使用标志 HWND_BOTTOM 的函数 SetWindowPos 应该可以帮助您。但是,根据我的经验:即使在某些用户操作的结果调用 SetWindowPos 后,您的窗口可能仍然会置于前台。


0
子类化窗体,覆盖WndProc函数并拦截负责在激活时将其向上移动z顺序的Windows消息。

0
创建一个覆盖在你的表单上的面板,但是你可以在面板上放置任何你想要的内容,然后在面板的点击事件中编写 this.sendback。

-2

我已经成功解决了使用setwindowpos时的闪烁问题...

const UInt32 SWP_NOSIZE = 0x0001;
const UInt32 SWP_NOMOVE = 0x0002;
const UInt32 SWP_NOACTIVATE = 0x0010;
const UInt32 SWP_NOZORDER = 0x0004;
const int WM_ACTIVATEAPP = 0x001C;
const int WM_ACTIVATE = 0x0006;
const int WM_SETFOCUS = 0x0007;
static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
const int WM_WINDOWPOSCHANGING = 0x0046;

[DllImport("user32.dll")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X,
   int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
static extern IntPtr DeferWindowPos(IntPtr hWinPosInfo, IntPtr hWnd,
   IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
static extern IntPtr BeginDeferWindowPos(int nNumWindows);
[DllImport("user32.dll")]
static extern bool EndDeferWindowPos(IntPtr hWinPosInfo);

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    IntPtr hWnd = new WindowInteropHelper(this).Handle;
    SetWindowPos(hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);

    IntPtr windowHandle = (new WindowInteropHelper(this)).Handle;
    HwndSource src = HwndSource.FromHwnd(windowHandle);
    src.AddHook(new HwndSourceHook(WndProc));
}

private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    if (msg == WM_SETFOCUS)
    {
        IntPtr hWnd = new WindowInteropHelper(this).Handle;
        SetWindowPos(hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
        handled = true;
    }
    return IntPtr.Zero;
}

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IntPtr windowHandle = (new WindowInteropHelper(this)).Handle;
    HwndSource src = HwndSource.FromHwnd(windowHandle);
    src.RemoveHook(new HwndSourceHook(this.WndProc));
}

你正在使用WindowInteropHelper,但没有提供它,这是毫无意义的。你不应该让用户去寻找一个包装类,希望它是你使用的正确的类。提供一切。 - ScottN
FYI,这是用于WPF应用程序的代码。WindowInteropHelper在using System.Windows.Interop;中。 - javon27

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接