Delphi Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
델파이 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
FreePascal/Lazarus
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
델마당
볼랜드포럼 광고 모집

델파이 팁&트릭
Delphi Programming Tip&Tricks
[295] 모니터 연결을 알아채고 제대로 처리하려면?
박지훈.임프 [cbuilder] 32465 읽음    2013-01-26 22:49
델파이와 C++빌더의 VCL에서는 이미 멀티모니터, 즉 둘 이상의 모니터를 지원하기 위한 훌륭한 준비가 되어 있습니다. TScreen 구조체가 그것인데요. TScreen 타입인 Screen 전역 객체를 참조하면 현재 시스템에 연결된 모니터들에 대한 정보들을 상세하게 알아낼 수 있습니다.

예를 들면 연결된 모니터들의 갯수를 알아내려면 Screen.MonitorCount를 읽어오면 되고, 특정 번호의 모니터에서 가로 해상도를 알려면 Screen.Monitors[I].Width를 읽으면 됩니다. Screen.Monitors[] 배열 속성은 TMonitor 타입의 클래스로서, 이 클래스는 지정한 모니터의 Top, Left, Width, Height는 물론 BoundsRect 등의 정보를 갖고 있습니다. 추가로 Windows API를 활용하기 위해선 HMONITOR 타입의 핸들인 Handle 속성을 이용할 수 있죠.

그런데 이렇게 잘 만들어진 TScreen과 TMonitor 구조에 한가지 크게 아쉬운 부분이 있습니다. 그것은, 새로운 모니터가 연결되거나 분리되었을 때 알려주는 이벤트가 없다는 것입니다. 그래서 이번 글에서는 모니터가 연결 또는 분리되었을 때 알아채는 방법과 그 후 추가로 필요한 처리에 대해 알아보겠습니다.

새로운 모니터가 연결 또는 분리되었을 때 알아채는 방법 자체는 간단합니다. Windows API의 WM_DISPLAYCHANGE 메시지를 이용하면 됩니다. 사실 이 메시지에 대한 마이크로소프트의 설명에서는 해상도가 변경되었을 때 발생한다고만 적혀 있는데요. 모니터가 연결되거나 분리될 때도 항상 발생합니다. (혹은, WM_SETTINGCHANGE 메시지를 처리해도 됩니다만, 이 WM_SETTINGCHANGE 메시지는 모니터와 관련된 메시지가 아닌 아주 다양한 경우에 발생하므로 아무래도 WM_DISPLAYCHANGE 메시지를 이용하는 것이 낫겠지요)

따라서, 아래와 같이 코딩을 하면 모니터가 새로 연결되거나 분리될 때 알아챌 수 있습니다.
먼저 델파이 코드입니다.
type
  TForm1 = class(TForm)
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    procedure WMDiaplayChange(var Message: TMessage); message WM_DISPLAYCHANGE;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.WMDiaplayChange(var Message: TMessage);
begin
  Memo1.Lines.Add('Monitor setting changed!');
end;


C++빌더라면 아래와 같이 메시지맵을 만들어야 합니다.
// 헤더파일의 폼 클래스
class TForm1 : public TForm
{
__published:	// IDE-managed Components
	TMemo *Memo1;
	void __fastcall FormCreate(TObject *Sender);
private:	// User declarations
	void __fastcall WMDiaplayChange(TMessage& Msg);
public:		// User declarations
	__fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(WM_DISPLAYCHANGE, TMessage, WMDiaplayChange)
END_MESSAGE_MAP(TForm)
};

// 폼의 cpp 파일
void __fastcall TForm1::WMDiaplayChange(TMessage& Msg)
{
	Memo1->Lines->Add("Monitor setting changed!");
}


사실 WM_DISPLAYCHANGE의 WPARAM과 LPARAM에는 모니터의 컬러 수와 가로세로 해상도가 전달됩니다만, 이게 유용한 것은 단일 모니터에서 해상도가 바뀌었을 때 뿐이므로 WPARAM과 LPARAM 인자는 무시하기로 합니다. 우리는 VCL의 친절한 Screen 객체로부터 이런 정보를 간단히 알아낼 수 있으니까요.

그런데 여기에 약간 곤란한 문제가 있습니다. VCL의 Screen 객체에는 모니터 연결/분리 이벤트만 구현되어 있지 않을 뿐만 아니라, Screen 객체는 아예 내부적으로도 모니터 연결/해제에 대한 구현이 되어 있지 않습니다. 따라서 모니터가 추가로 연결된 후에도 Screen.MonitorCount는 여전히 애플리케이션 시작 당시의 값, 예를 들면 1로 나온다는 것입니다. 2로 나오는 것이 당연한데도요.

사실 이 문제 하나만 해결하려고 한다면, Screen.MonitorCount 대신 GetSystemMetrics(SM_CMONITORS) 호출로 바꾸기만 하면 됩니다. GetSystemMetrics() 함수는 Windows API의 함수로서 여러 시스템 관련 정보들을 알아낼 수 있는데, 인자로 SM_CMONITORS를 넘기면 현재 연결된 모니터의 갯수를 리턴합니다. 따라서 당장 이 시점의 코드만 보자면 GetSystemMetrics(SM_CMONITORS) 호출로 바꿔버리면 됩니다. 그런데, Screen.MonitorCount가 제대로 업데이트되지 않으면 다른 문제들이 발생합니다.

VCL의 Screen.MonitorCount 속성과 Screen.Monitors[] 속성은 Screen 객체의 내부 객체인 FMonitors 객체로부터 참조하는 속성들입니다. Screen.FMonitors의 Count 값이 Screen.MonitorCount로 리턴되죠.

그런데 모니터가 새로 연결되거나 분리되었는데도 Screen.FMonitors.Count 값이 새로 업데이트되지 않은 상태로 남아있다면, 당연히 문제들이 발생합니다. 당장 새로 연결된 모니터의 정보가 Screen.Monitors[]에 반영되지 않을 것이며, 모니터 분리의 경우엔 Screen.Monitors[]가 존재하지 않는 메모리 객체를 참조하는 경우가 발생하게 되지요. 따라서 당연히 Access Violation 예외가 발생하게 됩니다.

따라서 Screen의 내부 객체 FMonitors를 반드시 업데이트해주어야 하는데요. 불행히도, TScreen 클래스에는 직접적으로 그런 역할을 해주는 public 메소드가 없습니다. TScreen.GetMonitors 메소드가 바로 그 역할을 합니다만 이게 private로 되어 있지요. private를 억지로 호출할 몇가지 방법이 있긴 합니다만 여기서는 피해갈 수 있는 더 좋은 방법이 있습니다.

애플리케이션 자체를 나타내는 VCL 전역 객체인 Application 객체에는 자체적인 WndProc 함수가 구현되어 있는데, 이 구현에서 WM_WTSSESSION_CHANGE 메시지를 받았을 때 Screen.GetMonitors를 호출하도록 되어 있습니다. (WM_WTSSESSION_CHANGE 메시지는 윈도우 OS에 새 사용자 세션이 열리거나 닫히거나 전환될 때 발생하는 메시지입니다)

또한 이 WndProc의 WM_WTSSESSION_CHANGE 메시지 처리에서는 Screen.GetMonitors를 호출하는 외에 다른 아무런 작업도 하지 않기 때문에 부작용의 우려도 없습니다. 따라서, Screen.FMonitors를 새로 업데이트하기 위한 목적으로 Application.Handle에 WM_WTSSESSION_CHANGE 메시지를 보내면 되며 다른 아무런 부작용도 없습니다. 따지자면 꽁수이긴 하지만 안전한 꽁수인 거죠.

아래는 델파이 코드입니다. (MultiMon을 uses 해야 합니다)
procedure TForm1.WMDiaplayChange(var Message: TMessage);
var
  I: Integer;
  MonitorInfoEx: TMonitorInfoEx;
begin
  inherited;
  SendMessage(Application.Handle, WM_WTSSESSION_CHANGE, 0, 0);  // Screen.FMonitors 업데이트
  Memo1.Lines.Add(Format('Monitor Count: %d', [Screen.MonitorCount]));
  for I := 0 to Screen.MonitorCount-1 do
  begin
    MonitorInfoEx.cbSize := sizeof(TMonitorInfoEx);
    GetMonitorInfo(Screen.Monitors[I].Handle, @MonitorInfoEx);
    Memo1.Lines.Add(Format('    Monitor %d [Handle %x] %s / %dx%d', 
      [I, Screen.Monitors[I].Handle, MonitorInfoEx.szDevice, 
      Screen.Monitors[I].Width, Screen.Monitors[I].Height]));
  end;
  Memo1.Lines.Add('');
end;


C++빌더 코드로는 아래와 같이 됩니다. (MultiMon.h을 include 해야 합니다)
void __fastcall TForm1::WMDiaplayChange(TMessage& Msg)
{
    SendMessage(Application->Handle, WM_WTSSESSION_CHANGE, 0, 0);
    Memo1->Lines->Add(String().sprintf(L"Monitor Count: %d", Screen->MonitorCount));
    MONITORINFOEXW MonitorInfoEx;
    for(int i=0; iMonitorCount; i++)
    {
        MonitorInfoEx.cbSize = sizeof(MONITORINFOEXW);
        GetMonitorInfoW(Screen->Monitors[i]->Handle, &MonitorInfoEx);
        Memo1->Lines->Add(String().sprintf(L"    Monitor %d [Handle %x] %s / %dx%d", 
            i, Screen->Monitors[i]->Handle, MonitorInfoEx.szDevice, 
            Screen->Monitors[i]->Width, Screen->Monitors[i]->Height));
    }
    Memo1->Lines->Add("");
}


이번의 코드에서, 각 모니터의 핸들을 출력했는데요. 테스트를 해본 결과, 새로운 모니터가 연결되었을 때 이전의 모니터의 핸들은 그대로 유지되지만, 모니터 하나가 분리되었을 때는 남아있는 기존 모니터의 핸들값까지 모두 바뀌어버리더군요. VCL 코드에서 그렇게 하는 것은 아니고, Windows API 자체가 그렇게 동작하고 있었습니다. 이 점에는 조금 주의가 필요하겠습니다.

아래는 위의 코드로 애플리케이션을 실행한 후, 제 PC의 모니터 두 개를 차례로 분리했다가 다시 연결했을 때의 실행 결과입니다.


소스코드와 실행파일을 다운로드하시려면 첨부파일을 클릭하세요. (XE2 버전에서 컴파일됨)


사실 애플리케이션 실행중인 상태에서 모니터가 새로 추가되거나 분리되는 경우는 그리 잦지는 않겠지요. 하지만 실제로 그런 일이 발생할 때 델파이/C++빌더의 VCL은 새로운 모니터의 연결이나 모니터 분리를 제대로 인식하지 못해 문제를 일으킬 수 있으며 일종의 VCL 버그라고 생각됩니다.

따라서 모니터의 추가, 분리에 대해 굳이 대응할 필요가 없다고 해도, WM_DISPLAYCHANGE 메시지를 처리해서 SendMessage(Application.Handle, WM_WTSSESSION_CHANGE, 0, 0); 실행은 해주어야 문제없이 동작할 것입니다. 그렇지 않으면 위에서 살펴본 대로 Access Violation 예외까지 일으키게 됩니다.

제 경우에는, 제 업무 프레임워크에서 윈도우 태블릿에서 외부 모니터를 연결하는 경우를 알아채고, 업무 시스템이 터치 기반의 태블릿 모드에서 넓은 스크린의 일반 데스크탑 모드로 전환되는 기능을 구상중이어서 그 과정에서 필요해서 찾아봤답니다. ^^
김태선 [cppbuilder]   2013-01-27 10:08 X
좋은 내용이군요. ㅋㅋ
Nibble [gameover]   2013-01-28 19:19 X
훌륭합니다.!
civilian [civilian]   2013-01-31 11:39 X
WMDisplayChange 메소드에 다음의 코드를 추가하면 좀더 편리하겠네요.

현재 화면이 다른 모니터에서 실행되다가 모니터가 분리된 경우 첫번째 모니터로 이동하도록...

  if (Screen.MonitorCount = 1) and (Monitor.MonitorNum > 0) then
  begin
    Left := Screen.Monitors[0].Left + Left;
  end;

+ -

관련 글 리스트
295 모니터 연결을 알아채고 제대로 처리하려면? 박지훈.임프 32465 2013/01/26
(링크)     C++Builder Tip'N Tricks > 모니터 연결을 알아채고 제대로 처리하려면?
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.