[C#] Window Form을 생성하지 않고 Window Control를 사용하는 방법


Development note/C#  2020. 5. 18. 16:44

안녕하세요. 명월입니다.


이 글은 C#에서 Window Form을 생성하지 않고 Window Control를 사용하는 방법에 대한 글입니다.


C#에서 Control 객체를 사용할 때, Window event message가 필요합니다.

Window event message가 무엇이냐면 Window OS 상에서 유저로 부터 이벤트를 받는 것, 즉 마우스를 클릭하거나 키보드를 누를 때 발생하는 이벤트를 큐 메시지로 받는 형태를 말합니다.

예를 들면 C++에서 GetMessage와 같은 것입니다.

while (GetMessage(&Message, NULL, 0, 0))
{
  // 키보드 메시지를 받고 프로그램에서 사용할 수 있게 변환한다.
  TranslateMessage(&Message);
  // 메시지를 WndProc로 보내는 함수.
  DispatchMessage(&Message);
}

링크 - [C++] C++에서 Window Form을 생성하는 방법(Win32Api, MFC)


C#에서는 이 Window event message를 받기 가장 쉬운 방법은 Window form 프로젝트를 생성해서 사용하는 게 가장 편합니다.

예를 들면 NotifyIcon 컨트럴을 사용하고 싶을 때 다음과 같이 작성됩니다.

using System;
using System.Drawing;
using System.Windows.Forms;
namespace Example
{
  // 윈도우 폼을 상속
  class Form1 : Form
  {
    private System.ComponentModel.IContainer components = null;
    // NotifyIcon 생성
    private NotifyIcon notifyIcon = new NotifyIcon();
    // 해제 시 동작
    protected override void Dispose(bool disposing)
    {
      // components 메모리 해제
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      // NotifyIcon 해제
      notifyIcon.Dispose();
      // 폼 해제
      base.Dispose(disposing);
    }
    // 기본 폼 컴퍼넌트 설정 (Window Form으로 프로젝트 설정하면 생성된다.)
    private void InitializeComponent()
    {
      // Container 생성
      this.components = new System.ComponentModel.Container();
      // 폰트 설정
      this.AutoScaleMode = AutoScaleMode.Font;
      // 폼 크기 설정
      this.ClientSize = new Size(800, 450);
      // 폼 타이틀 설정
      this.Text = "Form1";
    }
    // 폼 생성자
    public Form1()
    {
      // 컴퍼넌트 초기화
      InitializeComponent();
      // NotifyIcon 표시
      notifyIcon.Visible = true;
      // NotifyIcon 아이콘 설정
      notifyIcon.Icon = SystemIcons.WinLogo;
      // 오른쪽 마우스 클릭을 할 때 나오는 메뉴, exit를 누르면 폼이 종료된다.
      notifyIcon.ContextMenu = new ContextMenu(new MenuItem[]{ new MenuItem("exit", (s, ex) =>
      {
        this.Dispose();
      })});
      // 폼 비표시(동작하지 않는다.)
      this.Hide();
    }
  }
  // 실행 파일이 있는 클래스
  static class Program
  {
    // 단일 스레드 아파트먼트 정의
    [STAThread]
    // 실행 함수
    static void Main(string[] args)
    {
      // form 스타일을 기본으로 설정
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      // 메시지 루프를 생성
      Application.Run(new Form1());
    }
  }
}

폼이 생성되고 오른쪽 하단의 트레이를 보면 NotifyIcon이 생성되었습니다. NotifyIcon에 오른쪽 마우스 클릭을 하면 exit 메뉴가 생성되는데 이걸 클릭하면 폼이 종료가 됩니다.


그러나 서버 프로그램을 작성한다고 할 때, 사양에 따라서 이 Window Form이 필요가 없습니다. 오히려 화면 세션에 Form을 띄워야 하기 때문에 실수로 x버튼을 누르면 프로그램이 종료될 수도 있습니다.

물론, Dispose를 수정하면 종료까지는 실행되지 않습니다. 그러나 우리는 이 불필요한 Window Form을 처음부터 띄우고 싶지 않습니다.

여기서 위 소스에서도 Hide 함수를 사용했지만 실행 순서가 다른지 Hide를 해도 Window Form은 사라지지 않습니다. Thread나 Timer로 0.1초 후에 Hide를 실행하게 하면 사라지게는 할 수 있지만 실행할 때, 순간적으로 Window Form이 생성되었다 사라지는 모습을 볼 수 있습니다.

솔직히 Form이 필요가 없는데 선언한 것 자체가 눈에 거슬립니다.

그럼 그냥 Application.Run를 사용하지 않고 NotifyIcon를 사용하면 되지 않을까 싶은데.. 트레이에 생성은 됩니다만, 이벤트 동작을 하지 않습니다. 즉, 마우스 오른쪽 클릭을 해도 메뉴가 나오지 않습니다.


Form을 사용하지 않고 Window event message를 사용하는 방법은 두가지 방법이 있습니다.

먼저 ApplicationContext를 이용하는 방법입니다.

using System;
using System.Drawing;
using System.Windows.Forms;
namespace Example
{
  // 윈도우 폼 대신에 ApplicationContext를 상속
  class HideForm : ApplicationContext
  {
    // NotifyIcon 생성
    private NotifyIcon notifyIcon = new NotifyIcon();
    public HideForm()
    {
      // NotifyIcon 표시
      notifyIcon.Visible = true;
      // NotifyIcon 아이콘 설정
      notifyIcon.Icon = SystemIcons.WinLogo;
      // 오른쪽 마우스 클릭을 할 때 나오는 메뉴, exit를 누르면 폼이 종료된다.
      notifyIcon.ContextMenu = new ContextMenu(new MenuItem[]{ new MenuItem("exit", (s, ex) =>
      {
        // 메시지 루프 종료
        this.ExitThread();
      })});
    }
    // 해제 시 동작
    protected override void Dispose(bool disposing)
    {
      // NotifyIcon 해제
      notifyIcon.Dispose();
      // ApplicationContext 해제
      base.Dispose(disposing);
    }
  }
  // 실행 파일이 있는 클래스
  static class Program
  {
    // 단일 스레드 아파트먼트 정의
    [STAThread]
    // 실행 함수
    static void Main()
    {
      // 메시지 루프를 생성
      Application.Run(new HideForm());
    }
  }
}

이렇게 작성하면 Form을 실행하지 않고 NotifyIcon를 사용할 수 있습니다.


또 하나의 방법은 그냥 C++처럼 무한 루프로 메시지를 호출하는 것입니다.

using System;
using System.Drawing;
using System.Windows.Forms;
namespace Example
{
  // 실행 파일이 있는 클래스
  static class Program
  {
    // 단일 스레드 아파트먼트 정의
    [STAThread]
    // 실행 함수
    static void Main(string[] args)
    {
      // 메시지 루프 flag
      bool completed = false;
      // NotifyIcon 생성
      NotifyIcon notifyIcon = new NotifyIcon();
      // NotifyIcon 표시
      notifyIcon.Visible = true;
      // NotifyIcon 아이콘 설정
      notifyIcon.Icon = SystemIcons.WinLogo;
      // 오른쪽 마우스 클릭을 할 때 나오는 메뉴, exit를 누르면 폼이 종료된다.
      notifyIcon.ContextMenu = new ContextMenu(new MenuItem[]{ new MenuItem("exit", (s, ex) =>
      {
        // 메시지 루프 종료
        completed = true;
        // NotifyIcon 메모리 해제
        notifyIcon.Dispose();
      })});
      // 무한 루프.. exit를 눌러서 completed를 true로 바뀌지 않는 이상.
      while (!completed)
      {
        // 윈도우 메시지 실행
        Application.DoEvents();
      }
    }
  }
}

제가 예제로 NotifyIcon를 예제로 만들었지만, 의외로 COM의 컨트럴이나 윈도우 컨트럴 중에는 쓸만한 모듈이 많은데, Window message가 없으면 실행이 되지 않는 것들이 있습니다.

그 중에 WebBrowser가 대표적이겠네요. 보통 스크래핑 프로그램을 작성하는데 화면에서 스크래핑이 되는 것을 보기를 원하는 분도 있지만 내부 프로세스에서 작동되길 바라는 사람도 있습니다.

링크 - [C#] Gecko 라이브러리 (웹 스크래핑)

using System;
using System.Windows.Forms;
using Gecko;
using System.IO;
namespace Example2
{
  // 실행 파일이 있는 클래스
  static class Program
  {
    // 단일 스레드 아파트먼트 정의
    [STAThread]
    // 실행 함수
    static void Main()
    {
      // 메시지 루프 flag
      bool completed = false;
      Xpcom.EnableProfileMonitoring = false;
      // 프로그램 실행 디렉토리 취득
      var app_dir = Path.GetDirectoryName(Application.ExecutablePath);
      // COM DLL 초기화
      Xpcom.Initialize(Path.Combine(app_dir, "Firefox64"));
      // GeckoWebBrowser 생성
      GeckoWebBrowser browser = new GeckoWebBrowser();
      // 티스토리 블로그에 접속
      browser.Navigate("nowonbun.tistory.com");
      // 접속이 완료되는 시점의 이벤트
      browser.DocumentCompleted += (sender, e) =>
      {
        // 객체 취득
        GeckoWebBrowser b = (GeckoWebBrowser)sender;
        // Body html을 콘솔에 출력
        Console.WriteLine(b.Document.Body.OuterHtml);
      };
      // 무한 루프.. completed를 true로 바뀌지 않는 이상.
      while (!completed)
      {
        // 윈도우 메시지 실행
        Application.DoEvents();
      }
    }
  }
}

GeckoWebBrowser 객체가 Form이 없어도 실행이 되는 모습입니다.즉, Form이 없어도 Scrapping 소스를 구현할 수 있다는 의미입니다.


여기까지 C#에서 Window Form을 생성하지 않고 Window Control를 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.