hetepeperfan
hetepeperfan

Reputation: 4411

msys2 windows open in the background not at HWND_TOP

I'm happily using msys2 in combination with the pacman package manager to create a library. The library has a concept of windows, that is wrapped around HWND's and winuser.h's functions to create the window and window class. The windows are intended as a toplevel window. Normally I would suspect that a window, when opened for the first time, meets a Number of conditions:

  1. They are on top of the other (toplevel) windows. (this is - I think - what you get when you open e.g. chrome, word etc. via e.g. the windows taskbar or start menu.
  2. They are the ones that receive input from the keyboard.

No matter what I try, not one of the conditions above is met. The window is behind the msys2 terminal, but also all other opened windows. The icon of my program flickers with an orange color on the task bar at the bottom.

The class for my window is created as follows and is destroyed when the last of my windows is also destroyed.

static void
create_window_class(void)
{
    g_module_handle = GetModuleHandle(NULL);

    WNDCLASSEX win_class;
    memset(&win_class, 0, sizeof(win_class);
    win_class.cbSize        = sizeof(win_class);
    win_class.style         = CS_OWNDC;
    win_class.lpfnWndProc   = startup_winproc;
    win_class.hCursor       = LoadCursor(NULL, IDC_ARROW);
    win_class.lpszClassName = g_win_class_name;

    g_win_class  = (WNDCLASSEX*) g_malloc0(sizeof(WNDCLASSEX));
    *g_win_class = win_class;

    g_win_class_atom = RegisterClassEx(g_win_class);
    if (g_win_class_atom == 0) {
        char error_buf[1024];
        psy_strerr(GetLastError(), error_buf, sizeof(error_buf));
        g_critical("Unable to create WindowClass '%s': %s",
                   g_win_class_name,
                   error_buf);
    }
}

And then the window is created like this:

static void
win_window_constructed(GObject *object)
{
    PsyWinWindow *self = PSY_WIN_WINDOW(object);
    G_OBJECT_CLASS(psy_win_window_parent_class)->constructed(object);

    DWORD win_style = WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU | WS_CAPTION;
    DWORD win_style_ex = WS_EX_OVERLAPPEDWINDOW;

    gint nth_mon = psy_window_get_monitor(PSY_WINDOW(self));
    g_assert(nth_mon >= 0 && (guint) nth_mon < self->display_info->len);
    PsyDisplayInfo *desired_monitor = self->display_info->pdata[nth_mon];
    PsySize        *size            = desired_monitor->rect->size;
    PsyPos         *pos             = desired_monitor->rect->pos;

    RECT r;
    r.bottom = 480,    // pos->y + size->height;
    r.top    = pos->x, // pos->y;
    r.right  = 640,    // pos->x + size->width;
    r.left   = pos->y, // pos->x;

    // We could use AdjustWindowRectEx to set the client size to the desired
    // size however, we try just to create a window at fullscreen size
    // AdjustWindowRectEx(&r, win_style, FALSE, win_style_ex);

    self->window = CreateWindowEx(win_style_ex,
                                  g_win_class_name,
                                  "psy_window",
                                  win_style,
                                  r.left,
                                  r.top,
                                  r.right - r.left,
                                  r.bottom - r.top,
                                  NULL,
                                  NULL,
                                  g_module_handle,
                                  self);
    if (!self->window) {
        char error_buff[1024];
        psy_strerr(GetLastError(), error_buff, sizeof(error_buff));
        g_critical("Unable to create window: %s", error_buff);
        return;
    }

    BOOL success;
    ShowWindow(self->window, SW_SHOWMAXIMIZED);

    // Try to set it to the top in the Z-order, this functions
    // succeeds, however, the windows Z-order policy doesn't
    // allow the monitor to go to the top. Only once the user
    // clicks it, it will go to the top. Currently, the window
    // will flicker it's icon on the task bar.
    //
    // Also show the window.
    success = SetWindowPos(self->window,
                           HWND_TOP,
                           0,
                           0,
                           0,
                           0,
                           SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW);
    if (!success) {
        char error_buf[1024];
        psy_strerr(GetLastError(), error_buf, sizeof(error_buf));
        g_critical("Unable to set window pos: %s", error_buf);
    }

    success = SetForegroundWindow(self->window);
    if (!success) {
        char error[1024];
        psy_strerr(GetLastError(), error, sizeof(error));
        g_critical(
            "%s: Unable to set window to foreground: %s", __func__, error);
    }

    PsyD3dContext *context
        = psy_d3d_context_new_full(self, 1, self->enable_debug);
    psy_canvas_set_context(PSY_CANVAS(self), PSY_DRAWING_CONTEXT(context));
}

I run my programs using the msys2 terminal.

./some/path/myprogram.exe

I can see the window popping up, eh under that is. I see my program on the taskbar and if I use SetForegroundWindow(self->window), I can see the icon flickering in orange on the taskbar. I get messages such as WM_CREATE, WM_ACTIVATE (although I do get the message the keyboard remains with msys2 e.g pressing alt + f4 would close the msys2 terminal and not my window).

There are some corner cases, where the 2 points above are met.

  1. I run the command through gdb e.g. gdb ./some/path/myprogram.exe
  2. I run cmd and start the program from there, it works only the first time.
  3. I can use HWND_TOPMOST with SetWindowPosition, but that only makes the window appear above the others, even when I switch to another window with alt+tab or mouse click and even then the keyboard input isn't focused on my program. Besides, after the window is visible, I want users to be able to switch to a window of their choice. I have no intention of fighting the z-order of the windows.

Now it might have something to do with staring programs from the msys2 terminal. If I run chrome via the msys2 terminal it also seems to pop under the other windows.

I'm running this at windows 10, version 22H2

So my question boils down to: why do programs run via msys2 rather pop under in stead of pop up, and why isn't the keyboard focus directed to the newly opened windows?

edit (10-1-2025): The problem didn't seem to lie with the code above nor msys2, but with the meson development environment that I created. See my answer below.

Upvotes: 1

Views: 139

Answers (2)

hetepeperfan
hetepeperfan

Reputation: 4411

The problem I had was probably unrelated to both msys2 and the code that is in the question. As I'm developing a library I build my code with meson. Meson can create an environment in which I can run my executables.

meson devenv -C builddir

This opens a sub process in which the environment is setup, so that all libraries that I build are found, as well as additional dependencies from packages that I install with msys2 pacman/pacboy. So creating a development environment is incredibly handy for developing on windows. However, this seems to create a python sub process, hence it is a new process from which you start to debug. The original msys2 terminal is associated with it own foreground window, but the sub process is probably not related.

The 4 mandatory conditions are met:

  1. My program is a desktop app
  2. I don't think the foreground app (msys2 terminal) has called LockSetForgroundWindow()
  3. I don't think that foreground lock time-out has expired
  4. There are no menus active

The problem lied in the conditions of at least one must be met

  1. The calling process is the foreground process.
  2. The calling process was started by the foreground process.
  3. There is currently no foreground window, and thus no foreground process.
  4. The calling process received the last input event.
  5. Either the foreground process or the calling process is being debugged.

Typically, msys2 which has it's own window is the foreground process. So the 2nd condition is met. Unless you run meson setup -C my-build-dir this opens a sub process/shell inside the existing shell, which is not associated with a window, that is still the original process. Hence, condition 2 is not valid anymore. And like I wrote, I was still able to get a foreground window when I am debugging (even with gdb), so in that case I was able to get my windows to the foreground.

Upvotes: 1

許恩嘉
許恩嘉

Reputation: 478

Obviously, your program does not have the Foreground activation permission.

An application cannot force a window to the foreground while the user is working with another window(e.g. msys2 terminal). Instead, Windows flashes the taskbar button of the window to notify the user.
The documentation for the SetForegroundWindow function is very clear:

The system restricts which processes can set the foreground window. A process can set the foreground window by calling SetForegroundWindow only if:

All of the following conditions are true:

  • The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1.
  • The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function.
  • The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
  • No menus are active.

Additionally, at least one of the following conditions is true:

  • The calling process is the foreground process.
  • The calling process was started by the foreground process.
  • There is currently no foreground window, and thus no foreground process.
  • The calling process received the last input event.
  • Either the foreground process or the calling process is being debugged.

It is possible for a process to be denied the right to set the foreground window even if it meets these conditions.

I would suspect that a window, when opened for the first time, meets a Number of conditions:

  1. They are on top of the other (toplevel) windows. (this is - I think - what you get when you open e.g. chrome, word etc. via e.g. the windows taskbar or start menu.
  2. They are the ones that receive input from the keyboard.
  1. When you open chrome via the windows taskbar or start menu, the foreground window is taskbar or start menu. The process is started by the foreground process, so it can set the foreground window.
  2. Keyboard input is directed to the foreground window.

According to my actual measurements, even if the foreground window has been changed, sometimes a process that previously had Foreground activation permission still seems to be able to set the foreground window, even though the process and the process that started it are no longer the foreground process. I'm still not sure why. However, obviously, if your process has never had the Foreground activation permission, it is impossible for you to still have it when the conditions are not met.

Upvotes: 1

Related Questions