본문 바로가기
프로그래밍/Windows

[Windows Programming] 모니터 제어 명령 세트(MCCS)를 활용한 모니터 제어 프로그램

by Hwan,. 2022. 8. 7.
728x90
반응형

프로그램 제작 이유

 만약 PC와 모니터가 2대씩 있을 때 각 PC를 둘 다 듀얼 모니터로 사용하고 싶다면 어떻게 하는게 좋을까?
가장 쉽게 문제를 해결하려면 매번 사용하는 PC의 HDMI 케이블을 바꿔서 연결하면 된다!


만약 그것도 귀찮다면 KVM 스위치를 구매해 연결해두고 필요한 PC의 버튼이나 단축키를 눌러서 사용하면 된다.
(망 분리 환경이라면 보통 Aten과 같은 회사의 보안 인증이 된 제품을 지급해준다. 하지만 개인이 구매하기 어렵고 비싸다..!)
하지만 PC를 사용하다보면 양쪽 PC를 동시에 활용해야하는 경우가 꽤 많았고 개인적으로 KVM에서 듀얼 모니터 설정이 더 어려웠던 것 같다.

차라리 아래처럼 케이블을 전부 연결해두고 모니터 버튼으로 필요할 때마다 입력소스를 바꿔주는게 더 편하다.


많이 편해졌지만 여전히 몸을 움직여 버튼을 눌러야 하고 만약 PC 1을 듀얼로 사용하던 중에 PC 2를 듀얼로 사용하려면 버튼을 두번 눌러야 한다.

여러 대의 PC를 하나의 인터페이스로 다루는 KVM과 사용자의 편의를 위한 듀얼 모니터를 최소의 움직임으로 사용할 순 없는 걸까?

프로그램을 만들어보기로 했다.


 

MCCS, DDC/CI, VCP Code

 MCCS (Monitor Control Command Set)는 모니터 제어 명령 세트로 VESA (Video Electronics Standards Association)에서 개발한 컴퓨터 디스플레이 표준이다.
보통 PC나 셋탑박스 같은 장치에서 모니터를 제어하기 위한 바이너리 프로토콜로 사용되지만, 해당 글에서는 프로그램 제작을 위해 사용하려고 한다.

MCCS를 활용하면 DDC/CI (Display Data Channel/ Command Interface)와 VCP (Virtual Control Panel) 코드를 통해 모니터에 명령을 보낼 수 있다.

VCP 코드는 가상 제어 패널의 약어로 아래 이미지와 같은 표준 명령어 타입을 따른다.

VCP 코드


정해진 값을 약속된 프로토콜로 모니터에 명령을 전달하면 모니터가 해당 동작을 수행한다.
예를 들어 VCP 코드로 0xD6을 넣어주면 모니터의 전원이 On/Off되고, 0x60을 전달하면 모니터의 입력 소스가 변경된다.

현재 만들어지는 모니터는 대부분(거의 전부) 해당 표준을 따르기 때문에 코드만 잘 전달하면 정말 편할 것 같다..!
근데 모니터에 코드를 어떻게 전달하라는 걸까?

여러 방식이 있겠지만 난 보통 윈도우를 많이 사용하기 때문에 exe 프로그램을 제작하기로 했다.
Windows API를 통해 모니터로 VCP 코드를 보내보자.


 

모니터 제어 API

 일반적으로 모니터와 관련된 API를 검색하면 모니터의 핸들을 얻어오거나 정보를 얻어오기 위해 MONITORINFOEXA와 같은 구조체나 GetMonitorInfo()와 같은 함수를 사용한다. 하지만 제어를 위해서는 DDC/CI 프로토콜에 맞춰서 모니터로 데이터를 보내주어야 한다.

다행히 MSDN에서 DDC/CI를 사용하는 API들을 찾을 수 있었다. (만약 없었다면 따로 라이브러리(dll)가 존재하긴 한다.)
https://docs.microsoft.com/ko-kr/windows/win32/monitor/monitor-configuration

 

구성 모니터링(구성 모니터링) - Win32 apps

모니터 구성

docs.microsoft.com


MCCS 표준을 따르면 모니터 버튼으로 할 수 있는 동작 대부분을 프로그램으로 수행할 수 있지만 지금은 모니터 입력 소스만 정확하게 변경하면 된다.

위에서 VCP 코드를 DDC/CI 프로토콜에 따라서 모니터로 보내주기만 하면 기능이 수행된다고 했었다.
그런데 MSDN을 보면 API들은 내부적으로 DDC/CI를 사용하여 모니터에 명령을 보내준다고 되어 있기 때문에, VCP 코드만 제대로 맞춰서 전달해주면 될 것 같다.

이제 VCP 코드를 내가 원하는 모니터로 전달해주는 방법을 찾아야된다.


 

High-Level/ Low-Level Monitor 구성 함수

 위 MSDN 링크에 들어가보면 High-Level과 Low-Level로 나뉘어진 함수들을 볼 수 있는데, 이 중 나에게 필요한 내용은 VCP 코드를 다룰 수 있는 Low-Level 함수들이다.

MSDN의 Low-level Monitor 함수 사용법


대충 윈도우에서 감지한 모니터들을 알려주는 Enum 함수와 VCP 관련된 함수들을 알려주는데, 8번의 SetVCPFeature 함수가 눈에 띄였다.

https://docs.microsoft.com/en-us/windows/win32/api/lowlevelmonitorconfigurationapi/nf-lowlevelmonitorconfigurationapi-setvcpfeature

 

SetVCPFeature function (lowlevelmonitorconfigurationapi.h) - Win32 apps

Sets the value of a Virtual Control Panel (VCP) code for a monitor.

docs.microsoft.com

_BOOL SetVCPFeature(
  [in] HANDLE hMonitor,
  [in] BYTE   bVCPCode,
  [in] DWORD  dwNewValue
);


SetVCPFeature 함수는 위와 같은 형태로 되어 있다.
Enum 함수 등으로 얻어온 모니터의 핸들을 구해서 넣고 원하는 VCP Code 값을 넣으면 될거 같다.
이제 변경을 원하는 모니터와 원하는 VCP 기능을 전달하는 방법은 알았다.

그런데 우리의 상황처럼 1개의 모니터에 연결된 케이블이 여러 개라면 원하는 케이블은 어떻게 설정해야 되는 걸까?

이 방법을 알기 위해 엄청난 검색을 했는데..
방법은 의외로 간단했다.
3번째 인자인 dwNewValue에 해당 케이블의 값을 넣어주면 된다!
그렇다면 그 값은 어떻게 알 수 있을까??

다시 엄청난 삽질이 시작됐다.


 

GetVCPFeatureAndVCPFeatureReply() 함수

 온갖 키워드와 방법으로 검색해 알게된 방법은 GetVCPFeatureAndVCPFeatureReply() 함수를 사용하는 것 이었다.
그런데 위의 MSDN 이미지의 7번을 자세히 보면 이미 설명이 잘 되어 있다.
MS에선 처음부터 전부 다 알려주었지만 내가 이해를 못했을 뿐이다. (멍청하면 손발이 고생한다..)

https://docs.microsoft.com/en-us/previous-versions/ms775219(v=vs.85)

 

GetVCPFeatureAndVCPFeatureReply Function

Table of contents Monitor GetVCPFeatureAndVCPFeatureReply Function  Article 11/02/2006 2 minutes to read In this article --> Monitor Configuration API GetVCPFeatureAndVCPFeatureReply Function Retrieves the current value, maximum value, and code type of a

docs.microsoft.com

  BOOL GetVCPFeatureAndVCPFeatureReply(
    HANDLE
    hMonitor,

    BYTE
    bVCPCode,

    LPMC_VCP_CODE_TYPE
    pvct,

    LPDWORD
    pdwCurrentValue,

    LPDWORD
    pdwMaximumValue

  );


함수를 살펴보면 먼저 SetVCPFeature 함수와 비슷하게 모니터의 핸들과 VCP 코드를 요구한다.
그리고 3, 4번째 인자를 통해 Current Value 값과 Maximum Value 값을 얻어 낼 수 있었다.
(cf. 일반적으로 return을 통해 함수의 결과를 받지만 return으로는 하나의 값만 반환받을 수 있기 때문에 포인터 형태로 비어있는 변수를 전달해주면 알아서 채워준다.)

위의 함수를 잘 사용하면 모니터에 연결되어 있는 케이블들의 Value 값을 구할 수 있다.
(HDMI, DP, VGA등에 따라 값이 다르다.)

이제 SetVCPFeature() 함수를 사용하는데 필요한 모든 데이터를 구했다.

하지만 아직 마지막 한 가지 문제가 남아있다.
모니터의 제조사나 모델마다 해당 Value의 값이 다르다는 것이다ㅋㅋ
(예를 들면 A사의 A1 모니터는 HDMI의 Value가 10이고, B사의 B1모니터는 101이다. 심지어 같은 제조사의 다른 제품일 경우도 값이 다른 경우가 있었다.)

위의 API들을 잘 조합하면 자동으로 구할 수 있을 것 같았지만, 더 삽질하면 주말동안 끝내지 못할 것 같아 모니터들의 값을 알아낸 뒤 그냥 하드코딩했다.


 

코드

 먼저 프로그램의 기본 골격은 https://hwan001.tistory.com/1?category=1041434 글의 코드를 사용했다.
아무것도 없는 윈도우를 띄워주는 코드이다.

개발 환경은 Visual Studio Community 2022이고 프로젝트는 아래 이미지처럼 구성했다.

jsoncpp.cpp는 c++에서 json 파싱을 위해 나중에 추가한 라이브러리 코드이고 test.cpp는 테스트 함수를 작성해둔 파일이라 생략했다.
그리고 사실 EnumDisplayMonitors 함수는 MonitorEnumProc라는 콜백 함수를 내부에서 호출하기 때문에 SetVCPFeature 함수를 사용하기 위해서 콜백 함수들을 작성해야 하는데, 해당 내용들은 나중에 시간되면 추가로 작성해보겠다.

코드는 돌아가게만 만들어 두었기 때문에 정리도 잘 안되어 있지만 이것도 나중에 정리하기로 하고 일단 올려보겠다.


function.h 코드

#pragma once

#pragma warning(disable: 28251)

// api 헤더
#include <windows.h>
#include <winuser.h>
#include <Shlwapi.h>

#define VC_EXTRALEAN 
#define _WIN32_WINDOWS 0x0500

// library
#pragma comment(lib, "dxva2")
#pragma comment(lib, "user32")
#pragma comment(lib, "Shlwapi.lib")

// 모니터 vcp 관련 헤더
#include <lowlevelmonitorconfigurationapi.h>
#include <PhysicalMonitorEnumerationAPI.h>
#include <HighLevelMonitorConfigurationAPI.h>

// c++ 헤더
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include "json/json.h"

using namespace std;


// 구조체
typedef struct _MONITORPARAM
{
    LPCWSTR szPhysicalMonitorDescription;
    BYTE VCPcode;
    int source;
    BOOL bPowerOn;
    int curVal;
} MONITOR_PARAM, * PMONITOR_PARAM;

typedef struct _GETMONITORINFO
{
    LPCWSTR szPrimaryMonitorDescription;
    LPCWSTR szSecondMonitorDescription;
    int curVal;
    int curVal_second;
} GET_MONITOR_INFO, * GET_PMONITOR_INFO;


// 얘를 모니터 개수만큼 포인터 배열로 만들어서 _getCurrentValue에 전달하면, 모니터 돌면서 정보를 채워줌.
typedef struct _MONITOR{
    int num;
    char * monName;
    BYTE vcpValue;
} MONITOR, * PMONITOR;


// test 함수
VOID test_setInputSource(HWND, DWORD);
VOID test_getPrimaryMonitor(HWND);
VOID test_setInputSource(HWND, DWORD);
VOID test_SetMonitorPower(LPCWSTR, BOOL);

// interface 함수
VOID ChangeMonitorInput_hwan(PMONITOR mon, int monNum);
VOID GetMonitorInfo_hwan(PMONITOR mon, int monNum);

// callback 함수
BOOL CALLBACK MonitorEnumProc(HMONITOR, HDC, LPRECT, LPARAM);
BOOL CALLBACK _getCurrentValue(HMONITOR, HDC, LPRECT, LPARAM); // 얘가 모니터 정보 얻어와줌
BOOL CALLBACK _setMonitorInput(HMONITOR, HDC, LPRECT, LPARAM); // 얘는 특정 모니터에 value값 넣어줌.

// json 함수
void load_json(LPCWSTR file_path, PMONITOR mon, int monNum);
void save_json(LPCWSTR file_path, int monNum, Json::Value *monitor);


main.cpp 코드

#include "function.h"

#define BTN_SET 111
#define BTN_SAVE 112
#define EDIT_INFO 11
#define EDIT_INPUTNAME 21
#define EDIT_INPUTNUM 31
#define BTN_CLEAR 0

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

HINSTANCE g_Inst;
LPCWSTR lpszClass = TEXT("Change Monitor");

static BOOL mode; 
static TCHAR str_filename[1024];

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszClassParam, int nCmdShow)
{
    HWND hWnd;
    MSG Message;
    WNDCLASS WndClass;
    g_Inst = hInstance;
    TCHAR str_currentPath[1024];

    GetCurrentDirectory(1024, str_currentPath);
    wsprintf(str_filename, L"%s\\%s", str_currentPath, L"vcp.json");

    if (PathFileExists(str_filename)) {
        mode = TRUE;
    }
    else {
        mode = FALSE;
    }

    WndClass.cbClsExtra = 0;
    WndClass.cbWndExtra = 0;
    WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    WndClass.hInstance = hInstance;
    WndClass.lpfnWndProc = WndProc;
    WndClass.lpszClassName = lpszClass;
    WndClass.lpszMenuName = NULL;
    WndClass.style = CS_HREDRAW | CS_VREDRAW;

    RegisterClass(&WndClass);

    hWnd = CreateWindow(lpszClass, lpszClass, WS_OVERLAPPEDWINDOW
        , CW_USEDEFAULT, CW_USEDEFAULT, 500, 300
        , NULL, (HMENU)NULL, hInstance, NULL);
    
    if(!mode) ShowWindow(hWnd, nCmdShow);

    while (GetMessage(&Message, NULL, 0, 0))
    {
        TranslateMessage(&Message);
        DispatchMessage(&Message);
    }
    return (int)Message.wParam;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    static HWND hEdit_name, hEdit_num, btn_set, btn_save;
    static HWND hEdit_monitorNum;
    static HWND *hEdit_Monitor, *hEdit_inputName, *hEdit_inputNum;
    static int monNum;
    static PMONITOR mon;
    
    char str_monNum[1024], str_name[1024], str_num[1024];
    
    switch (iMessage)
    {
    case WM_CREATE:
        monNum = GetSystemMetrics(SM_CMONITORS);

        mon = (PMONITOR) malloc(sizeof(MONITOR) * monNum);
        for (int i = 0; i < monNum; i++) {
            mon[i].num = 0;
            mon[i].monName = new char[1024];
            mon[i].vcpValue = 0;
        }

        if (mode) {
            load_json(str_filename, mon, monNum);
            ChangeMonitorInput_hwan(mon, monNum);

            DestroyWindow(hWnd);
        }
        else {
            hEdit_Monitor = (HWND *)malloc(sizeof(HWND) * monNum);
            hEdit_inputName = (HWND *)malloc(sizeof(HWND) * monNum);
            hEdit_inputNum = (HWND *)malloc(sizeof(HWND) * monNum);

            for (int i = 0; i < monNum; i++) {
                hEdit_Monitor[i] = CreateWindow(L"edit", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY, 20, 10 + (i * 30), 300, 25, hWnd, (HMENU)(EDIT_INFO + i), g_Inst, NULL);
                hEdit_inputName[i] = CreateWindow(L"edit", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 20, (10 + (monNum * 30)) + (i * 30), 145, 25, hWnd, (HMENU)(EDIT_INPUTNAME + i), g_Inst, NULL);
                hEdit_inputNum[i] = CreateWindow(L"edit", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 175, (10 + (monNum * 30)) + (i * 30), 145, 25, hWnd, (HMENU)(EDIT_INPUTNUM + i), g_Inst, NULL);
            }
            
            btn_set = CreateWindow(L"button", L"Set", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 330, 10, 60, 25, hWnd, (HMENU)BTN_SET, g_Inst, NULL);
            btn_save = CreateWindow(L"button", L"Save", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 400, 10, 60, 25, hWnd, (HMENU)BTN_SAVE, g_Inst, NULL);

            // get monitor info
            GetMonitorInfo_hwan(mon, monNum);

            // set monitor info
            for (int i = 0; i < monNum; i++) {
                SetWindowTextA(hEdit_Monitor[i], mon[i].monName);
            }
        }
        break;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case BTN_SET:
            for (int i = 0; i < monNum; i++) {
                GetWindowTextA(hEdit_inputName[i], str_name, 1024);
                GetWindowTextA(hEdit_inputNum[i], str_num, 1024);

                MONITOR tmp_mon;
                tmp_mon.monName = new char[1024];
                wsprintfA(tmp_mon.monName, "%s", str_name);
                tmp_mon.vcpValue = atoi(str_num);

                ChangeMonitorInput_hwan(&tmp_mon, 1);

                delete tmp_mon.monName;
            }
            break;

        case BTN_SAVE:
            Json::Value valueMon[3];
            
            for (int i = 0; i < monNum; i++) {
                GetWindowTextA(hEdit_inputName[i], str_name, 1024);
                GetWindowTextA(hEdit_inputNum[i], str_num, 1024);

                valueMon[i]["name"] = str_name;
                valueMon[i]["value"] = atoi(str_num);
            }

            save_json(str_filename, monNum, valueMon);
            break;
        }
        break;

    case WM_PAINT:
        break;

    case WM_DESTROY:
        for (int i = 0; i < monNum; i++) {
            delete mon[i].monName;
        }

        free(mon);
        
        free(hEdit_Monitor);
        free(hEdit_inputName);
        free(hEdit_inputNum);

        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hWnd, iMessage, wParam, lParam);
}


function.cpp 코드

#include "function.h"


VOID ChangeMonitorInput_hwan(PMONITOR mon, int monNum)
{
    MONITOR tmp_mon;

    for (int i = 0; i < monNum; i++) {
        tmp_mon.monName = mon[i].monName;
        tmp_mon.vcpValue = mon[i].vcpValue;

        EnumDisplayMonitors(NULL, NULL, _setMonitorInput, (LPARAM)&tmp_mon);
    }
}


VOID GetMonitorInfo_hwan(PMONITOR mon, int monNum)
{
    MONITOR tmp_mon;

    for (int i = 0; i < monNum; i++) {
        tmp_mon.num = i;
        tmp_mon.monName = new char[1024];
        tmp_mon.vcpValue = -1;

        EnumDisplayMonitors(NULL, NULL, _getCurrentValue, (LPARAM)&tmp_mon);

        wsprintfA(mon[i].monName, "%s", tmp_mon.monName);
        mon[i].vcpValue = tmp_mon.vcpValue;

        delete tmp_mon.monName;
    }
}

BOOL CALLBACK _getCurrentValue(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwDate) {
    DWORD dwMonitorCapabilities = 0;
    DWORD dwSupportedColorTemperatures = 0;
    DWORD dwMonitorCount;
    DWORD curVal = -1;

    int len;
    char strMultibyte[1024];

    if (GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, &dwMonitorCount))
    {
        PHYSICAL_MONITOR* pMons;
        if (pMons = (PHYSICAL_MONITOR*)HeapAlloc(GetProcessHeap(), 0, sizeof(PHYSICAL_MONITOR) * dwMonitorCount))
        {
            if (GetPhysicalMonitorsFromHMONITOR(hMonitor, dwMonitorCount, pMons))
            {
                for (int i = 0; i < dwMonitorCount; i++)
                {
                    if (((PMONITOR)dwDate)->num == i) {
                        GetVCPFeatureAndVCPFeatureReply(pMons[i].hPhysicalMonitor, 0x60, NULL, &curVal, NULL);

                        len = WideCharToMultiByte(CP_ACP, 0, pMons[i].szPhysicalMonitorDescription, -1, NULL, 0, NULL, NULL);
                        WideCharToMultiByte(CP_ACP, 0, pMons[i].szPhysicalMonitorDescription, -1, strMultibyte, len, NULL, NULL);

                        wsprintfA(((PMONITOR)dwDate)->monName, "%s", strMultibyte);
                        ((PMONITOR)dwDate)->vcpValue = curVal;

                        DestroyPhysicalMonitor(pMons[i].hPhysicalMonitor);

                        break;
                    }
                }
            }

            HeapFree(GetProcessHeap(), 0, pMons);
        }
    }

    return TRUE;
}


BOOL CALLBACK _setMonitorInput(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwDate) {
    DWORD dwMonitorCapabilities = 0;
    DWORD dwSupportedColorTemperatures = 0;
    DWORD dwMonitorCount;
    DWORD curVal = -1;

    if (GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, &dwMonitorCount))
    {
        PHYSICAL_MONITOR* pMons;
        if (pMons = (PHYSICAL_MONITOR*)HeapAlloc(GetProcessHeap(), 0, sizeof(PHYSICAL_MONITOR) * dwMonitorCount))
        {
            if (GetPhysicalMonitorsFromHMONITOR(hMonitor, dwMonitorCount, pMons))
            {
                int len;
                char str_tmp[1024];
                char strMultibyte[1024];

                for (int i = 0; i < dwMonitorCount; i++) 
                {
                    len = WideCharToMultiByte(CP_ACP, 0, pMons[i].szPhysicalMonitorDescription, -1, NULL, 0, NULL, NULL);
                    WideCharToMultiByte(CP_ACP, 0, pMons[i].szPhysicalMonitorDescription, -1, strMultibyte, len, NULL, NULL);

                    if (strcmp(((PMONITOR)dwDate)->monName, strMultibyte) == 0) {
                        GetVCPFeatureAndVCPFeatureReply(pMons[i].hPhysicalMonitor, 0x60, NULL, &curVal, NULL);

                        if (curVal != -1)
                            SetVCPFeature(pMons[i].hPhysicalMonitor, 0x60, ((PMONITOR)dwDate)->vcpValue);
                    }

                    DestroyPhysicalMonitor(pMons[i].hPhysicalMonitor);
                }
            }

            HeapFree(GetProcessHeap(), 0, pMons);
        }
    }

    return TRUE;
}


void load_json(LPCWSTR file_path, PMONITOR mon, int monNum) {
    bool parsingSuccessful;

    Json::CharReaderBuilder builder;
    Json::CharReader* reader = builder.newCharReader();
    Json::Value root;
    string errors, text = "", line;

    ifstream jsonFile(file_path);
    if (jsonFile.is_open()) {
        
        while (getline(jsonFile, line)) {
            text += line;
        }

        jsonFile.close();
    }

    parsingSuccessful = reader->parse(text.c_str(), text.c_str() + text.size(), &root, &errors);
    delete reader;

    size_t rv = 0;
    for (Json::Value::const_iterator outer = root.begin(); outer != root.end(); outer++)
    {
        for (Json::Value::const_iterator inner = (*outer).begin(); inner != (*outer).end(); inner++)
        {
            for (int i = 0; i < monNum; i++) {
                if (outer.key().asString() == "monitor_" + to_string(i)) {
                    if (inner.key().asString() == "name") wsprintfA(mon[i].monName, "%s", inner->asCString());
                    if (inner.key().asString() == "value") mon[i].vcpValue = stoi(inner->asString());
                }
            }

        }
    }
}


void save_json(LPCWSTR file_path, int monNum, Json::Value *monitor) {
    Json::Value root;

    Json::Value MON;
    string key = "";

    for (int j = 0; j < monNum; j++) {
        key = "monitor_" + to_string(j);
        MON[key] = monitor[j];
    }

    root = MON;

    Json::StreamWriterBuilder writer;

    string str = Json::writeString(writer, root);

    ofstream write_JsonFile;
    write_JsonFile.open(file_path);
    if (write_JsonFile.is_open()) {
        write_JsonFile.write(str.c_str(), str.length());
    }

    write_JsonFile.close();
}

 

프로그램 설명

 프로그램을 시작할 때 동일 경로 상의 vcp.json 파일 존재를 기준으로 mode를 정하는데, 해당 mode가 활성화 되어 있을 경우 UI는 나타나지 않고 vcp.json에 정의된 대상으로 변경만 수행한 뒤 프로그램을 종료한다.
(최초에 한번 값을 넣어두면 다음부턴 실행했을 경우 모니터가 원하는 대상으로 변경된다. 개인적으로는 아래 작업 표시줄에 바로가기를 등록해두고 필요할때 눌러서 사용했다.)

여기에 등록해서 누르면 모니터가 바뀜


아래는 최초 실행 시 나오는 프로그램의 UI이다.

최초 실행 시 UI


현재는 연결된 모니터가 없어 노트북의 내장 모니터인 Generic PnP Monitor만 나오지만 다른 모니터가 연결되어 있을 경우, Dell H00000 (DP) 와 같은 식으로 모니터의 이름이 나온다.
(제대로 나오지 않을 경우 장치 관리자의 모니터 탭에서 확인할 수 있다.)

참고로 이 부분이 모니터 개수만큼 아래로 늘어난다.


프로그램에서는 해당 문자열을 1번에 입력하여 여러개의 모니터들 중 변경을 원하는 대상을 구분할 수 있고 2번에는 연결된 케이블의 Value을 입력하여 원하는 케이블을 선택할 수 있다.

입력 후 Set 버튼을 누르면 모니터가 변경되고, Save를 누르면 동일 경로에 입력된 값으로 vcp.json 파일이 생성된다
이후 프로그램을 종료하면 실행 시 마다 vcp.json 내의 값을 기준으로 모니터가 변경된다.
만약 값 변경을 원하면 vcp.json을 직접 수정하거나 파일을 삭제하고 프로그램을 재실행해서 입력해주면 된다.

추가로 Value를 구하는 방법이 궁금하면 위의 GetVCPFeatureAndVCPFeatureReply() 함수를 잘 활용해서 구해보기 바란다..
(만약 귀찮으면 1부터 Max까지 값을 직접 넣어보면서 바뀌는 값을 찾는 방법도 있다.)


 

결과 및 느낀점

 회사에 다니면서 주말 프로젝트로 간단하게 생각하고 진행했었는데, 자료를 찾다보니 점점 내용이 많아졌다.

어쨋든 기능적으로 나름 잘 돌아가는 프로그램을 만들었고 회사에서도 잘 사용을 하고 있기 때문에 마무리는 했지만, 중간에 작성했던 유용한 테스트 함수들이 계속 수정되면서 사라져 추가하지 못한 기능들이 많아 아쉬운 점이 많다. (Value 찾아주는 함수 등)

나중에 좀 더 기능을 추가해서 깔끔하게 업그레이드된 프로그램을 만들면 좋을 것 같다.

추가로 ddcutil? 라이브러리를 사용하면 dll을 통해 더 정리가 잘된 함수들을 사용할 수 있고 커맨드 형식으로도 제공이 되는 것 같다. (API로 삽질하지 말고 라이브러리 사용하면 편하다..)
https://www.ddcutil.com/

 

ddcutil Documentation

ddcutil and ddcui Announcements 05 August 2022 ddcui release 0.3.0 contains the following changes of interest to general users; CTL-Q terminates ddcui (does not apply within dialog boxes) Errors opening /dev/i2c and /dev/usb/hiddev devices are reported usi

www.ddcutil.com



그리고 전부는 아니지만 모니터 제조사와 모델별 특성과 값을 적어둔 페이지도 찾았다.
https://www.ddcutil.com/monitor_notes/

 

Notes on Specific Monitors - ddcutil Documentation

The following list describes some monitors that have been tested or reported by users. It highlights the variability in DDC implementation among monitors. VCP Version: 2.2 Controller Manufacturer: Mstar Controller Model: mh=0xff, ml=0x16, sh=0x00 Firmware

www.ddcutil.com

 

실행 파일은 깃허브에 올려두었다.

https://github.com/hwan001/Change_monitor_input

 

GitHub - hwan001/Change_monitor_input: 모니터 화면 전환 프로그램

모니터 화면 전환 프로그램. Contribute to hwan001/Change_monitor_input development by creating an account on GitHub.

github.com


728x90
반응형

댓글