Search code examples
windowswinapiprocesswindows-10multiple-monitors

How to programmatically start an application on a specific monitor on Windows 10?


I want to write a program that needs sometimes to start processes of another applications (mainly Sumatra PDF) on Windows 10, version 1803 (April 2018 Update).

These applications should be started on a specific monitor. I also want to be able to close the processes when needed.

The preferred languages are C# and Java, but any help is appreciated.

EDIT

I've tried to use the ShellExecuteExW function suggested by IInspectable in C++ code directly, but it doesn't work, as applications appear on the main monitor. I must have certainly made a mistake as I am absolutely new to WinAPI and know very little C++.

#include <Windows.h>

HMONITOR monitors[2]; // As it's only a test and I have currently only two monitors.
int monitorsCount = 0;

BOOL CALLBACK Monitorenumproc(HMONITOR hMonitor, HDC hdc, LPRECT lprect, LPARAM lparam)
{
    monitors[monitorsCount] = hMonitor;
    monitorsCount++;

    return TRUE;
}

int main()
{
    EnumDisplayMonitors(NULL, NULL, Monitorenumproc, 0);

    _SHELLEXECUTEINFOW info;
    ZeroMemory(&info, sizeof(info));
    info.cbSize = sizeof(info);
    info.fMask = SEE_MASK_HMONITOR;
    //info.lpVerb = L"open";
    info.lpFile = L"C:\\Windows\\System32\\cmd.exe";
    info.nShow = SW_SHOW;
    info.hMonitor = monitors[1]; // Trying to start on the second monitor.

    ShellExecuteExW(&info);

    return 0;
}

Solution

  • As suggested by others, this is intended behavior of Windows and for the good reasons.

    Also, you can not rely on default window placement atleast for SumatraPDF, since it surely does not use CW_USEDEFAULT, and instead stores these values in :

    %USERPROFILE%\AppData\Roaming\SumatraPDF\SumatraPDF-settings.txt

    There are multiple options though:

    1. Use third party tools that monitor top level windows and based on pre-configured rules moves them to specified display. E.g. DisplayFusion, etc.
    2. Use ligher weight solutions like AutoHotkey/AutoIt.
    3. Try and do this in code itself. Following is a working solution. I smoke tested on my box.

    Disclaimer: I have not written the entire code, for saving time I pulled it up from couple of sources, tweaked it, glued it together, and tested with SumantraPDF. Do also note, that this code is not of highest standards, but solves your problem, and will act as a can-be-done example.

    C++ code: (scroll down for C# code)

    #include <Windows.h>
    #include <vector>
    
    // 0 based index for preferred monitor
    static const int PREFERRED_MONITOR = 1;  
    
    struct ProcessWindowsInfo
    {
        DWORD ProcessID;
        std::vector<HWND> Windows;
    
        ProcessWindowsInfo(DWORD const AProcessID)
            : ProcessID(AProcessID)
        {
        }
    };
    
    struct MonitorInfo
    {
        HMONITOR hMonitor;
        RECT rect;
    };
    
    
    BOOL WINAPI EnumProcessWindowsProc(HWND hwnd, LPARAM lParam)
    {
        ProcessWindowsInfo *info = reinterpret_cast<ProcessWindowsInfo*>(lParam);
        DWORD WindowProcessID;
    
        GetWindowThreadProcessId(hwnd, &WindowProcessID);
    
        if (WindowProcessID == info->ProcessID)
        {
            if (GetWindow(hwnd, GW_OWNER) == (HWND)0 && IsWindowVisible(hwnd))
            {
                info->Windows.push_back(hwnd);
            }
        }
    
        return true;
    }
    
    
    BOOL CALLBACK Monitorenumproc(HMONITOR hMonitor, HDC hdc, LPRECT lprect, LPARAM lParam)
    {
        std::vector<MonitorInfo> *info = reinterpret_cast<std::vector<MonitorInfo>*>(lParam);
    
        MonitorInfo monitorInfo = { 0 };
    
        monitorInfo.hMonitor = hMonitor;
        monitorInfo.rect = *lprect;
    
        info->push_back(monitorInfo);
        return TRUE;
    }
    
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
    {
    
        // NOTE: for now this code works only when the window is not already visible
        // could be easily modified to terminate existing process as required
    
        SHELLEXECUTEINFO info = { 0 };
        info.cbSize = sizeof(info);
        info.fMask = SEE_MASK_NOCLOSEPROCESS;
        info.lpFile = L"C:\\Program Files\\SumatraPDF\\SumatraPDF.exe"; 
        info.nShow = SW_SHOW;
    
        std::vector<MonitorInfo> connectedMonitors;
    
        // Get all available displays
        EnumDisplayMonitors(NULL, NULL, Monitorenumproc, reinterpret_cast<LPARAM>(&connectedMonitors));
    
        if (ShellExecuteEx(&info))
        {
            WaitForInputIdle(info.hProcess, INFINITE);
    
            ProcessWindowsInfo Info(GetProcessId(info.hProcess));
    
            // Go though all windows from that process
            EnumWindows((WNDENUMPROC)EnumProcessWindowsProc, reinterpret_cast<LPARAM>(&Info.ProcessID));
    
            if (Info.Windows.size() == 1)
            {
                // only if we got at most 1 window
                // NOTE: applications can have more than 1 top level window. But at least for SumtraPDF this works!
    
                if (connectedMonitors.size() >= PREFERRED_MONITOR)
                {
                    // only move the window if we were able to successfully detect available monitors
    
                    SetWindowPos(Info.Windows.at(0), 0, connectedMonitors.at(PREFERRED_MONITOR).rect.left, connectedMonitors.at(PREFERRED_MONITOR).rect.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
                }
            }
    
            CloseHandle(info.hProcess);
        }
    
        return 0;
    }
    

    To emphasize one of my comments in code. This code will only work if the process in question is not already running. You can tweak the code as per your requirements otherwise.

    Update: Added C# code below, as I realized OP prefers C#. This code also has the termination logic cooked in.

    C# code:

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);
    
    private const int SWP_NOSIZE = 0x0001;
    private const int SWP_NOZORDER = 0x0004;
    
    private const int PREFERRED_MONITOR = 1;
    
    static void Main(string[] args)
    {
    
        // NOTE: you will have to reference System.Windows.Forms and System.Drawing (or 
        // equivalent WPF assemblies) for Screen and Rectangle
    
        // Terminate existing SumatraPDF process, else we will not get the MainWindowHandle by following method.
        List<Process> existingProcesses = Process.GetProcessesByName("SumatraPDF").ToList();
    
        foreach (var existingProcess in existingProcesses)
        {
            // Ouch! Ruthlessly kill the existing SumatraPDF instances
            existingProcess.Kill();
        }
    
        // Start the new instance of SumantraPDF
    
        Process process = Process.Start(@"C:\Program Files\SumatraPDF\SumatraPDF.exe");
    
        // wait max 5 seconds for process to be active
        process.WaitForInputIdle(5000);
    
    
        if (Screen.AllScreens.Length >= PREFERRED_MONITOR)
        {
            SetWindowPos(process.MainWindowHandle,
                IntPtr.Zero,
                Screen.AllScreens[PREFERRED_MONITOR].WorkingArea.Left,
                Screen.AllScreens[PREFERRED_MONITOR].WorkingArea.Top,
                0, 0, SWP_NOSIZE | SWP_NOZORDER);
        }
    }