WinForms 모달·모달리스 폼: ShowDialog와 Show (.NET 10)

WinForms 서브 폼을 ShowDialog(모달)와 Show(모달리스)로 여는 차이를 .NET 10에서 정리. DialogResult, 소유자, Dispose, 고DPI 기본값, ShowDialogAsync.

2026년 11월 보강: 2021년 글의 C# 예제를 .NET 10(WinForms, net10.0-windows)에서 다시 컴파일했습니다. WinForms 코드는 Windows 전용이라 Windows용으로 .NET 10에서 컴파일했고, 이 머신(macOS)에서는 실행하지 않았습니다. 그래서 이 글에는 실행 출력이 없고, 실제로 확인한 것은 컴파일 결과(경고·오류 코드), 템플릿이 생성하는 코드, 그리고 .NET 9와 .NET 10의 컴파일 차이입니다. 실행 시 동작은 Microsoft Learn 문서를 근거로 설명합니다. 2021년 글에 있던 Visual Studio 2019 화면 두 장은 뺐습니다(이유는 글 끝에).

이 글은 WinForms에서 메인 폼이 다른 폼(설정 창, 입력 창, 도구 창)을 띄울 때 ShowDialog와 Show 중 무엇을 써야 하는지 헷갈리는 분을 위한 글입니다. 결론은 간단합니다. 다음 줄을 실행하기 전에 사용자의 답이 필요하면 ShowDialog(this)를 쓰고 using으로 해제합니다. 사용자가 메인 폼과 오가며 함께 써야 하면 Show(this)를 쓰고, 결과는 이벤트로 받습니다.

모달과 모달리스: 2021년 예제를 .NET 10에서

2021년 글의 C# 예제는 그대로 컴파일됐습니다(경고 0개). 아래는 주석만 줄인 것입니다.

//Form2 인스턴스
Form2 f = new Form2();
//모달폼으로 열기, 소유자를 this로 설정
f.ShowDialog(this);
//창이 닫히면, 객체파기
f.Dispose();
//Form2 인스턴스
Form2 f = new Form2();
//모달리스 폼으로 열기
f.Show();

두 방식의 차이는 세 가지로 정리됩니다.

판단 그림. 질문: 다음 줄이 실행되기 전에 결과가 필요한가. 그렇다면 ShowDialog(this). 호출한 쪽은 기다리고, ShowDialog는 DialogResult를 돌려주며, 폼은 숨겨지기만 하므로 using var 등으로 직접 해제한다. 아니라면 Show(this). 호출 다음 코드가 바로 실행되고, 닫으면 폼이 해제되며, 결과는 FormClosed나 직접 만든 이벤트로 받는다.
ShowDialog와 Show의 차이는 세 가지다. 호출한 코드가 기다리는지, DialogResult가 돌아오는지, 그리고 폼을 누가 해제하는지.
  • 호출한 코드가 기다리는가. ShowDialog는 폼이 닫힐 때까지 돌아오지 않습니다. 그동안 같은 스레드의 다른 창(메인 폼 등)은 입력을 받지 않습니다. Show는 창을 띄우고 바로 돌아오므로 다음 줄이 즉시 실행됩니다.
  • 결과가 돌아오는가. ShowDialog는 DialogResult를 반환합니다. Show는 void입니다.
  • 누가 해제하는가. Microsoft Learn의 Form.ShowDialog 문서는 모달 폼이 닫기 버튼이나 DialogResult 설정으로 닫힐 때 Close가 아니라 숨겨지기만 한다고 설명합니다. 그래서 직접 Dispose해야 합니다. Form.Close 문서에도 "ShowDialog로 표시한 폼은 Close에서 해제되지 않는다"는 예외가 적혀 있습니다. 2021년 글의 "모달은 Dispose, 모달리스는 필요 없음"이라는 설명은 맞습니다.

2021년 본문에서 보충할 점은 "모달 폼이 열리면 그 폼 이외에는 접근할 수 없다"는 문장입니다. 정확히는 같은 UI 스레드의 창이 막히는 것이고, 다른 프로그램이나 다른 UI 스레드에서 만든 창은 그대로 쓸 수 있습니다.

2026년식: using, 소유자, 중복 방지

지금은 같은 일을 이렇게 씁니다. 아래 코드는 net10.0-windows에서 경고 0개로 컴파일됐습니다.

void OpenModal()
{
    using var dlg = new Form2();
    DialogResult result = dlg.ShowDialog(this);   // 여기서 대기, 소유자는 비활성
    Text = $"closed with {result}";
}                                                 // 여기서 Dispose

Form2? _tool;
void OpenModeless()
{
    if (_tool is { IsDisposed: false }) { _tool.Activate(); return; }
    _tool = new Form2();
    _tool.FormClosed += (_, _) => _tool = null;
    _tool.Show(this);                              // 소유됨: 항상 위, 함께 최소화
}
  • using var를 쓰면 예외가 나도 해제가 보장됩니다. 2021년 코드는 ShowDialog 안에서 예외가 나면 Dispose를 건너뜁니다.
  • 2021년 모달리스 예제는 f.Show()로 소유자 없이 열었습니다. 그러면 메인 폼을 클릭하는 순간 서브 폼이 뒤로 숨고, 버튼을 누를 때마다 창이 하나씩 늘어납니다. Show(this)로 소유자를 지정하고, 이미 열려 있으면 Activate()만 하는 것이 보통의 도구 창 동작입니다. "폼을 하나만 띄우기"는 폼을 하나만 표시하기 글에서 자세히 다룹니다.
  • 모달리스 폼의 결과는 ShowDialog처럼 받을 수 없으므로 FormClosed 이벤트나 직접 만든 이벤트로 넘깁니다. 이벤트 선언은 이벤트 글을 참고하세요.

DialogResult를 OK/취소 버튼과 연결하는 방법, 그리고 입력이 잘못됐을 때 닫히지 않게 막는 방법은 OK·취소 버튼 글에서 이어서 다룹니다.

.NET 10 템플릿과 고DPI 기본값

2021년 글은 Visual Studio 2019의 ".NET Framework용 Windows Forms 앱" 템플릿으로 시작했습니다. .NET 10에서는 dotnet new winforms로 만들 수 있고, 생성되는 Program.cs는 다음과 같습니다(실제 생성 결과).

[STAThread]
static void Main()
{
    // To customize application configuration such as set high DPI settings or default font,
    // see https://aka.ms/applicationconfiguration.
    ApplicationConfiguration.Initialize();
    Application.Run(new Form1());
}

ApplicationConfiguration.Initialize()는 소스 생성기가 빌드 때 만들어 주는 메서드입니다. EmitCompilerGeneratedFiles=true로 빌드해서 생성된 파일을 꺼내 보면 내용은 이렇습니다.

public static void Initialize()
{
    global::System.Windows.Forms.Application.EnableVisualStyles();
    global::System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
    global::System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.SystemAware);
}

기본값은 SystemAware 였습니다. 즉 앱이 시작될 때의 주 모니터 배율로 그리고, 배율이 다른 모니터로 창을 옮기면 Windows가 비트맵을 늘려서 흐릿해질 수 있습니다. 프로젝트 파일에 한 줄을 넣고 다시 빌드하면 생성 코드가 바뀝니다.

<PropertyGroup>
  <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>
global::System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);

Microsoft Learn의 "High DPI support in Windows Forms" 문서는 모니터마다 배율이 다른 환경에서 PerMonitorV2를 권합니다. 대화 상자를 메인 폼과 다른 모니터에 띄우는 일이 흔하므로, 모달·모달리스를 쓰는 앱이라면 한 번 확인할 가치가 있습니다. 또 Application.SetHighDpiMode 문서에 따르면 이 설정은 첫 창을 만들기 전에 호출해야 효과가 있습니다. Initialize()보다 먼저 폼을 new 하는 코드(예: 정적 필드에서 폼 생성)가 있으면 안 됩니다.

async와 ShowDialog: 2021년 무렵의 함정

2021년에는 "무거운 작업을 비동기로 돌리고 끝나면 대화 상자를 띄우기"를 하다가 흔히 걸리는 함정이 있었습니다. 셋 다 지금도 그대로 유효합니다.

  1. Task.Run 안에서 ShowDialog를 부르기. 폼이 스레드 풀 스레드(MTA)에서 만들어집니다. WinForms는 Main에 [STAThread]가 붙은 STA 스레드를 전제로 하고, 컨트롤은 만든 스레드에서만 다뤄야 합니다. 그래서 UI 스레드의 폼을 소유자로 넘기면 크로스 스레드 호출이 되고, 클립보드·끌어서 놓기 같은 OLE 기능은 STA가 아니라는 예외로 실패합니다.
  2. UI 스레드에서 .Result나 .Wait()로 기다리기. UI 스레드가 막혀서 await 뒤의 이어지는 코드가 UI 스레드로 돌아오지 못하고 교착 상태가 됩니다.
  3. 올바른 방법은 이벤트 핸들러를 async로 만들고 await 뒤에서 ShowDialog를 부르는 것. await가 끝나면 WinForms의 동기화 컨텍스트 덕분에 UI 스레드로 돌아오므로 그대로 폼을 띄울 수 있습니다.
async void OpenAfterLoadAsync()
{
    string data = await Task.Run(() => "loaded");  // await 뒤에는 UI 스레드
    using var dlg = new Form2 { Text = data };
    dlg.ShowDialog(this);
}

.NET 9부터는 Form.ShowDialogAsync가 추가됐습니다. 참조 팩의 문서 주석에는 "폼이 닫히거나 해제되면 완료되는 Task<DialogResult>를 반환한다"고 적혀 있습니다. 같은 코드를 두 버전으로 빌드해 보니 결과가 달랐습니다.

async Task OpenModalAsync()
{
    using var dlg = new Form2();
    DialogResult r = await dlg.ShowDialogAsync(this);
    Text = r.ToString();
}
# net9.0-windows (Microsoft.WindowsDesktop.App.Ref 9.0.22)
Program.cs(69,32): error WFO5002: 'System.Windows.Forms.Form.ShowDialogAsync(System.Windows.Forms.IWin32Window)'은(는) 평가 목적으로 제공되며, 이후 업데이트에서 변경되거나 제거될 수 있습니다. 계속하려면 이 진단을 표시하지 않습니다.

# net10.0-windows (Microsoft.WindowsDesktop.App.Ref 10.0.14)
Build succeeded. 0 Warning(s) 0 Error(s)

.NET 9에서는 실험적 API라 경고가 아니라 오류로 빌드가 멈췄고, .NET 10에서는 진단 없이 컴파일됐습니다. 실행해서 확인한 것은 아니므로 동작은 문서 설명까지만 말할 수 있습니다.

정리

상황 쓸 것
답을 받은 뒤 다음 처리 using var dlg = …; dlg.ShowDialog(this)
메인 폼과 함께 쓰는 도구 창 Show(this) + 이미 열려 있으면 Activate()
모달리스 폼의 결과 FormClosed 또는 직접 만든 이벤트
비동기 작업 후 대화 상자 async 핸들러에서 await 뒤 ShowDialog (.NET 10은 ShowDialogAsync도 가능)
여러 모니터 <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>

2021년 글의 Visual Studio 2019 "새 프로젝트 만들기" 화면 두 장은 삭제했습니다. 빨간 테두리로 강조한 구성과 파일 이름이 Microsoft 문서의 튜토리얼 이미지와 같아 직접 찍은 것인지 확인할 수 없었고(저작권 위험), .NET Framework 템플릿을 고르는 화면이라 내용도 더 이상 맞지 않습니다. 개발 환경 설치는 Visual Studio 설치 글에서 다룹니다. VB.NET 예제는 C#과 같은 API를 쓰므로 이번 판에서는 C#만 남겼습니다.

확인 환경: .NET SDK 10.0.403, Microsoft.WindowsDesktop.App.Ref 10.0.14(비교용 9.0.22), C# 14, macOS(Apple Silicon Mac). WinForms 코드는 dotnet build -p:EnableWindowsTargeting=true로 컴파일만 했습니다.

자주 묻는 질문

ShowDialog로 연 폼은 꼭 Dispose 해야 하나요?

네. Microsoft Learn의 Form.ShowDialog 문서에 따르면 모달로 연 폼은 닫기 버튼을 누르거나 DialogResult가 정해져도 Close가 불리지 않고 숨겨지기만 합니다. 다시 표시할 수 있는 대신 직접 Dispose 해야 하므로 using var dlg = new Form2(); 처럼 쓰는 것이 간단합니다. Show로 연 모달리스 폼은 닫힐 때 자동으로 해제됩니다.

Show(this)와 Show()는 무엇이 다른가요?

Show(this)는 호출한 폼을 소유자(Owner)로 지정합니다. 소유된 폼은 항상 소유자 위에 표시되고, 소유자를 최소화하거나 닫으면 함께 최소화되거나 닫힙니다. Show()만 쓰면 메인 폼을 클릭했을 때 서브 폼이 뒤로 숨어 버릴 수 있습니다.

.NET 10에서 ShowDialogAsync를 그냥 써도 되나요?

net10.0-windows(참조 팩 10.0.14)에서 Form.ShowDialogAsync는 경고 없이 컴파일됐습니다. 같은 코드를 net9.0-windows로 빌드하면 error WFO5002(평가 목적으로만 제공)로 실패합니다. .NET 9를 대상으로 해야 한다면 진단을 억제해야 하고, .NET 10 이상이라면 일반 API처럼 쓸 수 있습니다.

  • #C#
  • #WinForms
  • #ShowDialog
  • #modal
  • #modeless

t.laboratory

직접 만들고 운영하는 앱·서버·서비스에서 겪은 문제와, 그 해결에 쓴 코드를 기록하는 기술 블로그입니다.

운영자 소개 · 앱

↑