ディスプレイの有効/無効を切り替えるコンソールアプリを作った話

# モチベ
普段ディスプレイを3枚使っているけれど、場面によってオン/オフする画面の枚数を変えたい。
構成は以下の通り

A. ゲーミングモニタ(プライマリ)
B. 4Kモニタ
C. FHDモニタ

これらを場面によって使い分けている。
普段はPCのグラボからすべての各ディスプレイに映像を出力している。
今回、以下の要件を満たすソフトを開発した。

1.PS5で遊びたいときはPCから4Kモニタへの映像出力は停止して、PS5から4Kモニタへ映像を流したい。
2.リモートワーク中はPCからFHDモニタへの映像出力は停止して、リモートワーク用PCからFHDへ映像を流したい。

今まで、いちいちNVIDIAコンパネを起動して、無効にするディスプレイを選択して、適用して...
とかなりクリック数が必要で何気にストレスだったので、ホットキーで簡単に切り替えられるようにした。

# Win32APIで作った
VS2022 C++ ConsoleApplicationで作成しました。
完全に自分用なので汎用的に作られていないところがあります。

:::note warn
注意
この実装方法だとなぜかプライマリディスプレイのオンオフはできない。
詳しく調べてないのでわからないけど、1枚はオンっぱなにしておくためのフールプルーフのような仕組みなんだろうか...
:::

exeファイルをショートカットキーとかで割り当てておけば、1ボタンでオンオフができる...幸せや....
```:実行方法
# 1枚目(4Kディスプレイ)をオフする。すでにオフ状態なら拡張モードで表示する。
> DiscplaySwitchConsoleApplication.exe 1

# 引数無しで実行すると、どのディスプレイをトグルするか選択できる。
> DiscplaySwitchConsoleApplication.exe
> TOGGLE DISPLAY : 2
```

簡単なフロー
(実行時引数あり == トグルするディスプレイをdisplayNameTableの添字で指定)
1. displayNameTableに登録されているディスプレイ名を使って、Windowsが制御しているディスプレイ情報から対応するハンドラのインデックスを見つけます。
2. 1.で取得したインデックスを基に出力の有無を判断する。(オンならオフ、オフならオンに)

## コード本体

```C++:DiscplaySwitchConsoleApplication.cpp

#include <stdio.h>
#include <iostream>
#include <tchar.h>
#include <vector>
#include <windows.h>
#include <WinUser.h>

void toggleDisplayActivate(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index);
void displayDisable(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index);
void displayEnable(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index);
int searchDisplayIndex(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int targetDisplayIndex);

#define DISPLAY_NUM 3
const wchar_t* displayNameTable[DISPLAY_NUM] = {
        L"ZOWIE XL LCD",
        L"BenQ EL2870U",
        L"BenQ GW2470",
};

int main(int argc, char* argv[])
{
    UINT32 PathCount = 0;
    UINT32 ModeCount = 0;
    HRESULT hr;
    hr = GetDisplayConfigBufferSizes(QDC_ALL_PATHS, &PathCount, &ModeCount);
    std::vector<DISPLAYCONFIG_PATH_INFO> pathArray(PathCount);
    std::vector<DISPLAYCONFIG_MODE_INFO> modeArray(ModeCount);
    hr = QueryDisplayConfig(QDC_ALL_PATHS, &PathCount, &pathArray[0], &ModeCount, &modeArray[0], NULL); // QDC_ONLY_ACTIVE_PATHS推奨
    int index = 0;

    if (argc == 1) {
        /* トグルしたい物理ディスプレイのインデックス */
        printf("TOGGLE DISPLAY : ");
        scanf_s("%d",&index);
    }
    else if (argc == 2) {
        /* トグルしたい物理ディスプレイのインデックス */
        index = strtol(argv[1], NULL, 10);
    }
    else {
        printf("[INVALID PARAM] usage : DisplaySwitchConsoleApplication.exe [TURN ON/OFF DISPLAY_NUM]\n");
        return -1;
    }

    /* ディスプレイのインデックス */
    index = searchDisplayIndex(PathCount, pathArray, ModeCount, modeArray, index);

    /* 指定したディスプレイ出力をトグルする
     * もし指定ディスプレイが見つからない場合何もしない */
    if (index == -1) {

    }
    else {
        toggleDisplayActivate(PathCount, pathArray, ModeCount, modeArray, index);
    }
    
    return 0;
}

void toggleDisplayActivate(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index)
{
    if (pathArray[index].flags == 0) {
        displayEnable(PathCount, pathArray, ModeCount, modeArray, index);
    }
    else {
        displayDisable(PathCount, pathArray, ModeCount, modeArray, index);
    }
}

void displayDisable(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index)
{
    HRESULT hr;

    pathArray[index].flags = 0;
    hr = SetDisplayConfig(PathCount, &pathArray[0], ModeCount, &modeArray[0], SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES);
}

void displayEnable(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int index)
{
    HRESULT hr;

    // ディスプレイを有効
    pathArray[index].flags = 1;
    hr = SetDisplayConfig(0, NULL, 0, NULL, SDC_APPLY | SDC_TOPOLOGY_EXTEND);

    switch (hr) {
    case ERROR_SUCCESS:
        printf("ERROR_SUCCESS\n");
        break;
    case ERROR_INVALID_PARAMETER:
        printf("ERROR_INVALID_PARAMETER\n");
        break;
    case ERROR_ACCESS_DENIED:
        printf("ERROR_ACCESS_DENIED\n");
        break;
    case ERROR_GEN_FAILURE:
        printf("ERROR_GEN_FAILURE\n");
        break;
    case ERROR_BAD_CONFIGURATION:
        printf("ERROR_BAD_CONFIGURATION\n");
        break;
    default:
        printf("UNKNOWN ERROR : %d\n", hr);
        break;
    }
}

int searchDisplayIndex(UINT32 PathCount, std::vector<DISPLAYCONFIG_PATH_INFO>pathArray, UINT32 ModeCount, std::vector<DISPLAYCONFIG_MODE_INFO> modeArray, int targetDisplayIndex)
{
    int index;

    // ディスプレイ名を検索、比較
    for (index = 0; index < PathCount; index++)
    {
        DISPLAYCONFIG_TARGET_DEVICE_NAME dn;
        dn.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
        dn.header.adapterId = pathArray[index].targetInfo.adapterId;
        dn.header.id = pathArray[index].targetInfo.id;
        dn.header.size = sizeof(dn);
        DisplayConfigGetDeviceInfo((DISPLAYCONFIG_DEVICE_INFO_HEADER*)&dn);
        //wprintf(L"DISPLAY %d : %s\n", index, dn.monitorFriendlyDeviceName);
        if (wcscmp(displayNameTable[targetDisplayIndex], dn.monitorFriendlyDeviceName) == 0) {
            break;
        }
    }

    /* 見つからなかった場合 */
    if (index == DISPLAY_NUM) {
        index = - 1;
    }

    return index;
}

```

## 使ったWin32API

### GetDisplayConfigBufferSizes function (winuser.h)
https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdisplayconfigbuffersizes

呼び出し元は GetDisplayConfigBufferSizes を使用して、呼び出し元が QueryDisplayConfig の CCD 関数に必要な情報を取得できます。

### QueryDisplayConfig function (winuser.h)
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-querydisplayconfig

現在の設定ですべてのディスプレイの表示パスに関する情報を取得します。
私は書いてしまっていますが、第1引数に QDC_ALL_PATHS を指定するのは
リファレンスにも書いてある通り重い処理らしいのであまり使うのはおすすめしません。
有効になっているディスプレイの情報のみを取得したい場合は QDC_ONLY_ACTIVE_PATHS を指定しましょう。

### SetDisplayConfig function (winuser.h)
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setdisplayconfig

この関数を通してディスプレイを操作する。
例えば、接続されているディスプレイ全てを有効にして拡張モードで表示するときは以下を実行するだけ。
すっげぇ簡単でひっくり返った。

```C++:接続されているディスプレイ全てを有効にして拡張モードで表示
SetDisplayConfig(0, NULL, 0, NULL, SDC_APPLY | SDC_TOPOLOGY_EXTEND);
```

第1～第4引数が何なのかよくわからないまま開発できてしまったので詳しくは説明できないが、
ディスプレイをオフにするときにpathArray->flagsを0にする必要があるため、はじめに QueryDisplayConfig() で取得しておいてある。

# 参考まとめ
## Win32API
https://docs.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdisplayconfigbuffersizes

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-querydisplayconfig

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setdisplayconfig

[c++ - How to properly use SetDisplayConfig with multiple monitors? - Stack Overflow](https://stackoverflow.com/questions/57136638/how-to-properly-use-setdisplayconfig-with-multiple-monitors)

[モニタ名を取得する(2014年8月23日)](https://www.asmusic.jp/ASHARD/score/asscore.cgi/yu-oishi/20140823)

## nvAPI
実は最初はNVIDIAのSDKを使って作れないかと企んでいたので、nvapiでの映像出力についても調べていた...
もし作るならこの辺が役に立ちそう

NvAPI_DISP_SetDisplayConfig()でできるよ^^って言ってる
[How to apply enable/disable of DisplayId - Other Tools / NVAPI - NVIDIA Developer Forums](https://forums.developer.nvidia.com/t/how-to-apply-enable-disable-of-displayid/49286)

OFF->ONはNvAPI_CreateDisplayFromUnAttachedDisplay()でできるらしい。
[winapi - Enable/Disable multiple monitors via Win32 API or NVidia API? - Stack Overflow](https://stackoverflow.com/questions/6653217/enable-disable-multiple-monitors-via-win32-api-or-nvidia-api)