Windows Forms 시작하기, 폼 만들기와 도구 상자 컨트롤
목차
이 글은 C# 콘솔 입출력, ReadLine과 출력 서식에서 이어집니다. 지금까지는 검은 콘솔 창에 글자를 출력하고 입력을 받았습니다. 이번 글부터는 창과 버튼이 있는 Windows 프로그램을 만듭니다. Visual Studio의 Windows Forms 디자이너로 폼에 컨트롤을 끌어다 놓고, 버튼을 누르면 실행될 코드를 씁니다. 마지막에는 메뉴와 크기, 옵션을 골라 주문 목록에 담는 커피 주문 폼을 만듭니다.
먼저 이 글의 그림과 결과가 무엇인지 밝혀 둡니다. 예제는 .NET 10 SDK로 직접 빌드했습니다. 폼 그림은 실제 창을 찍은 화면이 아니라, 컨트롤 배치를 보여 주는 구조 그림입니다.
- 폼 프로젝트는 모두 경고 0개로 빌드에 성공했습니다. 빌드 결과와 일부러 틀리게 고쳐 본 빌드의 오류 메시지는 실제 출력입니다.
- 화면과 상관없는 가격 계산 코드는 콘솔 프로젝트로 실행해 확인했습니다. 이 글에 나오는 실행 결과는 그 콘솔 출력뿐입니다.
- 폼이 어떻게 보이고 움직이는지(컨트롤 배치, 메시지 상자 모양, 창 크기를 바꿀 때의 동작)는 코드가 그렇게 동작하도록 쓴 것이고, 실제 화면은 다를 수 있습니다.
Form1.Designer.cs는 Visual Studio 디자이너가 만드는 코드 모양을 따라 손으로 썼습니다. 위치·크기 숫자는 VS에서 끌어다 놓으면 다를 수 있습니다.- Visual Studio 메뉴와 창 이름은 공식 문서를 기준으로 적었습니다. 문서에 한국어 표시가 없는 이름은 영어 이름을 먼저 적었고, 한국어 표시는 환경에 따라 다를 수 있습니다.
1. Windows Forms 앱 프로젝트 만들기

Windows Forms(줄여서 WinForms)는 Windows 데스크톱 프로그램을 만드는 .NET의 화면 기술입니다. 프로젝트는 콘솔 앱처럼 Visual Studio의 새 프로젝트 만들기에서 만듭니다. Visual Studio를 설치할 때 고른 .NET 데스크톱 개발(.NET desktop development) 워크로드에 템플릿이 들어 있습니다.
만드는 순서
- Visual Studio를 열고 새 프로젝트 만들기(Create a new project)를 고릅니다.
- 템플릿 검색 칸에
winforms를 입력하고, 언어 드롭다운에서 C#을 고릅니다. - 목록에서 Windows Forms App을 고르고 다음을 누릅니다. 한국어 VS에서는 「Windows Forms 앱」처럼 보일 수 있으며, 한국어 표시는 버전에 따라 다를 수 있습니다.
- 새 프로젝트 구성(Configure your new project)에서 프로젝트 이름을
FirstForm으로 적고 다음을 누릅니다. - Additional information 화면에서 프레임워크를 .NET 10.0(장기 지원)으로 고르고 만들기를 누릅니다. 이 화면의 한국어 이름은 문서마다 다르게 번역되어 있어 버전에 따라 다를 수 있습니다.
Form1디자이너가 열립니다. 디자이너가 보이지 않으면 솔루션 탐색기에서Form1.cs를 더블클릭합니다.
검색 결과에는 이름이 비슷한 템플릿이 하나 더 나옵니다. 이름 끝에 (.NET Framework)가 붙은 템플릿은 옛 .NET Framework용이므로 피하고, 이름에 .NET Framework가 없는 「Windows Forms 앱」(.NET) 템플릿을 고릅니다.
만들어지는 프로젝트 파일
템플릿이 만드는 FirstForm.csproj입니다. 이 글의 예제는 dotnet new winforms 템플릿(SDK 10.0.401)이 만든 것과 같은 내용입니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
콘솔 앱 프로젝트 파일과 비교하면 세 줄이 다릅니다.
| 설정 | 폼 앱 | 콘솔 앱 | 뜻 |
|---|---|---|---|
| OutputType | WinExe |
Exe |
창 프로그램입니다. 실행해도 콘솔 창이 같이 뜨지 않습니다 |
| TargetFramework | net10.0-windows |
net10.0 |
.NET 10에 Windows 전용 기능을 더합니다 |
| UseWindowsForms | true |
(없음) | Windows Forms 라이브러리를 씁니다 |
Nullable과 ImplicitUsings는 콘솔 앱과 같습니다. 프로젝트 파일을 직접 고칠 일은 거의 없지만, 옛 글을 따라 고치다가 -windows를 지우면 빌드가 실패합니다. 실제로 net10.0-windows를 net10.0으로 바꾸고 빌드한 결과입니다. 첫 줄은 한국어 환경에서, 둘째 줄은 같은 빌드를 영어 환경에서 다시 돌린 결과이고 앞쪽 경로는 줄였습니다.
Microsoft.NET.Sdk.DefaultItems.targets(156,5): error NETSDK1136: Windows Forms 또는 WPF를 사용하거나 그러한 작업을 수행하는 프로젝트 또는 패키지를 참조하는 경우 대상 플랫폼을 Windows로 설정해야 합니다(일반적으로 TargetFramework 속성에 '-windows' 포함).
Microsoft.NET.Sdk.DefaultItems.targets(156,5): error NETSDK1136: The target platform must be set to Windows (usually by including '-windows' in the TargetFramework property) when using Windows Forms or WPF, or referencing projects or packages that do so.
Windows Forms는 Windows 전용 기술이라 대상 플랫폼에 Windows가 들어가야 한다는 뜻입니다. 이런 오류가 나면 TargetFramework에 -windows가 붙어 있는지 확인합니다.
폴더 구조
VS 기본 위치에 만들었을 때의 폴더 구조입니다. 실행 결과가 아니라 설명용 그림입니다.
C:\Users\(사용자)\source\repos\
└─ FirstForm\ ← 솔루션 폴더
├─ FirstForm.slnx ← 솔루션 파일(.sln일 수도 있음)
└─ FirstForm\ ← 프로젝트 폴더
├─ FirstForm.csproj
├─ Program.cs ← 프로그램 시작점(Main)
├─ Form1.cs ← 내가 코드를 쓰는 파일
├─ Form1.Designer.cs ← 디자이너가 쓰는 파일
├─ bin\Debug\net10.0-windows\ ← 실행 파일 위치
└─ obj\
콘솔 앱은 실행 파일이 bin\Debug\net10.0에 생겼는데, 폼 앱은 net10.0-windows 폴더에 생깁니다. 솔루션 탐색기에서는 Form1.Designer.cs가 Form1.cs 아래에 접혀 보입니다. 폼에 그림 같은 리소스를 넣으면 Form1.resx 파일도 생깁니다.
헷갈리기 쉬운 점
- 템플릿 이름 끝을 확인합니다. (.NET Framework)가 붙은 템플릿으로 만들면 프로젝트 파일 모양과
Program.cs가 이 글과 다릅니다. 이미 만들었다면 지우고 다시 만드는 편이 빠릅니다. - 프레임워크 목록에서 .NET 10.0을 고릅니다. 다른 버전을 고르면 프로젝트 파일의
net10.0-windows자리가 다른 숫자가 됩니다. 이 글의 코드는 .NET 10 기준입니다. - 믿을 수 있는 프로젝트만 엽니다. 공식 문서에 따르면 디자이너는 폼을 그릴 때 프로젝트 코드를 실행합니다. 인터넷에서 받은 남의 WinForms 프로젝트는 내용을 확인한 뒤 엽니다.
- .resx 파일은 디자이너로만 고칩니다. 공식 문서는 폼의 .resx 파일을 디자이너 밖에서 고치면 바꾼 내용이 사라질 수 있다고 안내합니다.
2. 디자이너·도구 상자·속성 창·솔루션 탐색기

공식 자습서는 폼을 만들 때 쓰는 Visual Studio 창을 네 개로 설명합니다.
| 창 | 하는 일 | 여는 법 |
|---|---|---|
| 솔루션 탐색기(Solution Explorer) | 프로젝트의 파일과 폼이 모두 보입니다 | 보기 메뉴, Ctrl+Alt+L |
| 폼 디자이너(Form designer) | 폼을 그리는 곳입니다. 컨트롤을 골라 옮기고 크기를 바꿉니다 | Form1.cs 더블클릭, Shift+F7 |
| 도구 상자(Toolbox) | 폼에 넣을 수 있는 컨트롤 목록입니다 | 보기 메뉴, Ctrl+Alt+X |
| 속성 창(Properties) | 고른 컨트롤의 설정을 바꿉니다 | 보기 메뉴, F4 |
공식 자습서에 따르면 Visual Studio는 처음에 도구 상자와 속성 창을 열지 않습니다. 보기 메뉴에서 두 창을 열어 두면 다음부터는 VS가 기억합니다. 폼 디자이너의 한국어 이름은 문서마다 "양식 디자이너", "Windows Forms 디자이너"처럼 다르게 번역되어 있습니다. 이 글에서는 폼 디자이너라고 부릅니다.
도구 상자
- 컨트롤을 폼으로 끌어다 놓거나 더블클릭하면 폼에 추가됩니다.
- 도구 상자는 VS 왼쪽에 접혀 있다가 마우스를 올리면 나타납니다. 고정 아이콘(핀)을 누르면 열린 채로 있습니다.
- 도구 상자 안의 분류 탭은 영어 VS에서 All Windows Forms, Common Controls, Containers 같은 이름입니다. 한국어 표시는 버전에 따라 다를 수 있으니, 탭을 찾기보다 위쪽 검색 칸에
Button처럼 컨트롤 이름을 입력하는 편이 빠릅니다. - 도구 상자에는 지금 보고 있는 디자이너에서 쓸 수 있는 컨트롤만 보입니다.
- 도구 상자가 사라졌으면 Window > Reset Window Layout(창 레이아웃 다시 설정)으로 VS 창 배치를 처음처럼 되돌립니다. 한국어 메뉴 표시는 버전에 따라 다를 수 있습니다.
속성 창
- 위쪽 목록에 지금 고른 개체의 이름이 보입니다. 여러 컨트롤을 함께 고르면 공통 속성만 보입니다.
- 정렬 단추가 두 개 있습니다. Categorized는 범주별로, Alphabetical은 속성 이름의 ABC 순으로 보여 줍니다. 한국어 VS의 단추 이름은 버전에 따라 다를 수 있습니다.
- 속성(Properties) 단추와 이벤트(Events) 단추가 있습니다. 이벤트 단추를 누르면 그 컨트롤의 이벤트 목록이 나옵니다. 이벤트 목록은 5장에서 씁니다.
- 속성 이름은
Text,BackColor처럼 영어 이름으로 적었습니다. 한국어 VS에서 어떻게 보이는지는 버전에 따라 다를 수 있습니다. 컨트롤의 이름을 정하는 Name 속성은 목록에서 (Name)처럼 괄호가 붙어 보일 수 있습니다. - 맨 아래 설명 칸에 고른 속성의 짧은 설명이 나옵니다.
디자이너에서 할 수 있는 일
- 컨트롤을 마우스나 화살표 키로 옮깁니다. Ctrl+화살표는 더 세밀하게 옮기고, Shift+화살표는 크기를 바꿉니다.
- 여러 컨트롤을 고르려면 Shift나 Ctrl을 누른 채 클릭하거나, 빈 곳에서 끌어 사각형으로 둘러쌉니다.
- 폼을 더블클릭하면 폼의
Load이벤트 코드로, 컨트롤을 더블클릭하면 그 컨트롤의 기본 이벤트 코드로 갑니다. 5장에서 자세히 봅니다. - 코드 보기는 F7, 디자이너 보기는 Shift+F7입니다.
디자이너의 실제 모습은 공식 자습서 Windows Forms 앱 만들기의 그림에서 볼 수 있습니다.
헷갈리기 쉬운 점
- 도구 상자가 비어 보이면 디자이너 화면인지 확인합니다. 도구 상자는 지금 디자이너에서 쓸 수 있는 컨트롤만 보여 줍니다.
Form1.cs코드 화면을 보고 있다면 Shift+F7로 디자이너로 돌아갑니다. - 속성 창은 지금 고른 것의 속성을 보여 줍니다. 버튼을 바꾸려 했는데 폼 제목이 바뀌었다면 폼이 선택되어 있던 것입니다. 속성 창 위쪽의 개체 이름을 먼저 확인합니다.
- Name과 Text는 다릅니다. Text는 화면에 보이는 글자이고, Name은 코드에서 부르는 이름입니다. 버튼 글자를 "인사하기"로 바꿔도 코드에서는 여전히 Name으로 부릅니다.
3. Form1.cs·Form1.Designer.cs·Program.cs
![Form1.cs와 Form1.Designer.cs 그림(한 클래스를 두 파일에 나눠 씀). '시작은 Program.cs': [STAThread], static void Main(), ApplicationConfiguration.Initialize();, Application.Run(new Form1()); 아래 회색 상자 'Initialize() 안에서 하는 일(생성 코드)': EnableVisualStyles, SetCompatibleTextRenderingDefault, SetHighDpiMode. new Form1() 화살표가 보라색 점선 상자 'partial class Form1'을 가리킴. 그 안에 초록 'Form1.cs, 내가 쓰는 코드(이벤트 처리기)': public partial class Form1 : Form, public Form1(), InitializeComponent();, btnHello_Click(...). 빨간 'Form1.Designer.cs, 디자이너가 쓰는 코드, 직접 고치지 않기': partial class Form1, private void InitializeComponent(), btnHello = new Button();, btnHello.Text = '인사하기';, private Button btnHello; InitializeComponent() 호출과 정의가 화살표로 이어지고 '컴파일하면 Form1 클래스 하나가 돼요'. 'Name = 코드에서 쓰는 변수 이름': 속성 창에서 바꾼 값 Name: btnHello가 private Button btnHello; 줄이 됨. 맨 아래 여자아이가 'Designer.cs를 직접 고쳐도 돼요?' 하고 로봇이 '디자이너가 다시 써요. 디자이너·속성 창으로!'라고 답함](https://blog.kakaocdn.net/dna/8zDGr/dJMcagtIs77/AAAAAAAAAAAAAAAAAAAAABjnE4zfcLvnUY6EIoX0ud2Ak1GmQo9MsmENVCOKDTcn/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1793458799&allow_ip=&allow_referer=&signature=NKBV87NszGbkC%2BTnigFsO2UpGuQ%3D)
프로젝트를 만들면 코드 파일이 세 개 생깁니다. 이름을 입력하고 버튼을 누르면 인사말을 보여 주는 작은 폼 FirstForm으로 세 파일을 차례로 봅니다.
Program.cs, 프로그램의 시작점
namespace FirstForm;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[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());
}
}
템플릿이 만든 그대로입니다. 지금까지의 콘솔 예제는 최상위 문으로 썼지만, Windows Forms 템플릿은 static void Main()을 시작점으로 씁니다.
[STAThread]는 창 프로그램에 필요한 표시입니다. 지우지 않습니다.ApplicationConfiguration.Initialize()는 화면 표시 방식을 준비하는 한 줄입니다. 이 메서드는 직접 쓰지 않아도 빌드할 때 자동으로 만들어집니다.Application.Run(new Form1())은Form1을 만들어 띄우고, 그 폼을 닫을 때까지 프로그램을 계속 돌립니다. 콘솔 앱처럼Main이 바로 끝나지 않는 이유입니다.
빌드할 때 자동으로 만들어진 ApplicationConfiguration 코드를 실제로 꺼내 보면 아래와 같습니다.
// <auto-generated />
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows.Forms;
/// <summary>
/// Bootstrap the application configuration.
/// </summary>
[CompilerGenerated]
internal static partial class ApplicationConfiguration
{
/// <summary>
/// Bootstrap the application as follows:
/// <code>
/// global::System.Windows.Forms.Application.EnableVisualStyles();
/// global::System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
/// global::System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.SystemAware);
/// </code>
/// </summary>
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);
}
}
옛 .NET Framework 글에 나오는 Application.EnableVisualStyles();, Application.SetCompatibleTextRenderingDefault(false); 두 줄과 고해상도 화면 설정이 이 한 줄에 들어 있습니다. 옛 블로그 코드와 모양이 달라도 하는 일은 같습니다.
함께 만들어지는 global using 목록도 꺼내 봤습니다. 프로젝트 파일의 ImplicitUsings 때문에 들어가는 using입니다.
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.Drawing;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Windows.Forms;
System.Windows.Forms와 System.Drawing이 들어 있으므로, 파일 맨 위에 using을 쓰지 않아도 Button, MessageBox, Color, Point를 바로 쓸 수 있습니다.
Form1.cs, 내가 쓰는 코드
namespace FirstForm;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// 디자이너에서 btnHello를 더블클릭하면 만들어지는 Click 이벤트 처리기
private void btnHello_Click(object sender, EventArgs e)
{
string name = txtName.Text.Trim();
if (name == "")
{
MessageBox.Show("이름을 입력해 주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtName.Focus(); // 커서를 이름 칸으로
return;
}
lblResult.Text = $"안녕하세요, {name}님!";
MessageBox.Show($"{name}님, Windows Forms에 오신 것을 환영합니다.", "인사");
}
}
public partial class Form1 : Form은 "Form 클래스를 상속한 내 클래스 Form1"입니다. 생성자는 InitializeComponent(); 한 줄을 부르는데, 이 메서드는 Form1.cs가 아니라 Form1.Designer.cs에 있습니다. btnHello_Click은 버튼을 누를 때 실행되도록 만든 메서드입니다. 이름 칸이 비어 있으면 메시지 상자로 알리고, 이름이 있으면 아래쪽 레이블에 인사말을 쓰고 메시지 상자를 띄우도록 썼습니다.
Form1.Designer.cs, 디자이너가 쓰는 코드
디자이너에서 컨트롤을 놓고 속성을 바꾸면 VS가 이 파일에 코드를 씁니다. 이 글의 예제는 VS가 만드는 모양을 따라 손으로 썼습니다. 위치·크기 숫자는 VS에서 끌어다 놓으면 다를 수 있습니다.
아래는 93줄짜리 파일에서 모양을 보여 주는 줄만 그대로 옮긴 것이고, // ... (생략) ... 자리는 뺀 부분입니다. 전체 파일은 디자이너가 만들어 주며, 뺀 부분(lblName, txtName, lblResult의 속성 블록과 Dispose 메서드)도 같은 모양이 반복됩니다.
namespace FirstForm;
partial class Form1
{
// ... (생략) ...
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
lblName = new Label();
txtName = new TextBox();
btnHello = new Button();
lblResult = new Label();
SuspendLayout();
// ... (생략) ...
//
// btnHello
//
btnHello.Location = new Point(217, 12);
btnHello.Name = "btnHello";
btnHello.Size = new Size(75, 23);
btnHello.TabIndex = 2;
btnHello.Text = "인사하기";
btnHello.UseVisualStyleBackColor = true;
btnHello.Click += btnHello_Click;
// ... (생략) ...
//
// Form1
//
AcceptButton = btnHello;
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(304, 81);
Controls.Add(lblResult);
Controls.Add(btnHello);
Controls.Add(txtName);
Controls.Add(lblName);
Name = "Form1";
Text = "첫 번째 폼";
ResumeLayout(false);
PerformLayout();
}
// ... (생략) ...
private Label lblName;
private TextBox txtName;
private Button btnHello;
private Label lblResult;
}
- 두 파일 모두
partial class Form1입니다.partial은 한 클래스를 여러 파일에 나눠 쓴다는 뜻이고, 컴파일하면Form1클래스 하나가 됩니다. 그래서Form1.cs에서 이 파일에 선언된txtName,lblResult를 바로 씁니다. InitializeComponent는 디자이너에서 놓은 컨트롤을 코드로 만듭니다.new Label()로 만들고, 위치·크기·글자를 정하고,Controls.Add로 폼에 넣습니다.- 맨 아래
private Label lblName;같은 필드가 컨트롤 변수입니다. 속성 창의 Name 값이 곧 이 변수 이름입니다. InitializeComponent위의 "Required method for Designer support - do not modify" 주석은 이 메서드를 코드 편집기로 고치지 말라는 뜻입니다. 디자이너가 이 파일을 다시 쓰기 때문에 손으로 고친 내용은 사라질 수 있습니다. 바꿀 것이 있으면 디자이너와 속성 창으로 바꿉니다.AcceptButton = btnHello;는 폼의 AcceptButton 속성입니다. 공식 문서에 따르면 다른 버튼이나 여러 줄 텍스트 상자에 포커스가 있을 때를 빼고, Enter 키를 누르면 이 버튼이 눌린 것으로 처리됩니다. 이름을 쓰고 Enter만 눌러도 인사가 나오도록 넣은 설정입니다.AutoScaleMode = AutoScaleMode.Font는 실행하는 PC의 글꼴 크기나 화면 배율이 디자인할 때와 다르면 폼 크기를 비율대로 조정하는 설정입니다.
빌드 결과
이 예제를 빌드한 결과입니다. 빌드 > 솔루션 빌드(Ctrl+Shift+B)로 빌드하면 프로젝트 폴더의 bin\Debug\net10.0-windows\에 아래 파일이 생깁니다.
빌드 성공 (경고 0개)
출력 폴더 bin\Debug\net10.0-windows\:
FirstForm.deps.json
FirstForm.dll
FirstForm.exe
FirstForm.pdb
FirstForm.runtimeconfig.json
FirstForm.exe가 실행 파일이고, 우리가 쓴 코드는 FirstForm.dll에 들어 있습니다. F5나 Ctrl+F5로 실행하면 Visual Studio가 빌드한 뒤 이 프로그램을 실행합니다.
헷갈리기 쉬운 점
- Form1.Designer.cs는 직접 고치지 않습니다. 열어 보고 이해하는 것은 좋지만, 고칠 때는 디자이너와 속성 창을 씁니다. 5장의 처리기 오류를 고칠 때가 거의 유일한 예외입니다.
- InitializeComponent를 지우지 않습니다. 생성자에서 이 줄을 지우면 컨트롤이 만들어지지 않습니다. 생성자에 코드를 더할 때는 이 줄 아래에 씁니다.
- ApplicationConfiguration 파일은 프로젝트에 보이지 않습니다. 빌드할 때 만들어지는 코드라 솔루션 탐색기에 파일이 없습니다. 없다고 직접 만들 필요는 없습니다.
- 옛 글의 this.는 없어도 됩니다. 옛 디자이너 코드는
this.btnHello.Text = ...처럼this.를 붙여 썼습니다. 붙여도 안 붙여도 뜻은 같습니다.
4. 자주 쓰는 컨트롤 13종과 속성

도구 상자에는 컨트롤이 많지만, 처음에는 아래 13개면 대부분의 연습 프로그램을 만들 수 있습니다. "더블클릭 이벤트"는 디자이너에서 그 컨트롤을 더블클릭하면 만들어지는 이벤트입니다. Windows Forms 10.0.12 참조 어셈블리의 DefaultEvent 특성을 직접 읽어 확인한 값입니다.
| 컨트롤 | 용도 | 꼭 알 속성 | 더블클릭 이벤트 | 이름 예 |
|---|---|---|---|---|
| Label | 글자 표시(사용자가 못 고침) | Text, AutoSize |
Click | lblName |
| TextBox | 글자 입력 | Text, Multiline, ReadOnly, TextAlign |
TextChanged | txtName |
| Button | 누르는 버튼 | Text |
Click | btnOk |
| CheckBox | 켜고 끄기, 여러 개 동시 선택 | Checked |
CheckedChanged | chkShot |
| RadioButton | 여러 개 중 하나만 선택 | Checked |
CheckedChanged | rdoSmall |
| ComboBox | 펼쳐서 하나 고르기 | Items, SelectedIndex, DropDownStyle |
SelectedIndexChanged | cboMenu |
| ListBox | 목록 보여 주기·고르기 | Items, SelectedIndex |
SelectedIndexChanged | lstOrders |
| NumericUpDown | 숫자를 화살표로 올리고 내리기 | Value, Minimum, Maximum |
ValueChanged | nudQty |
| DateTimePicker | 날짜·시간 고르기 | Value, Format, CustomFormat |
ValueChanged | dtpPickup |
| PictureBox | 그림 표시 | Image, SizeMode |
Click | picCoffee |
| GroupBox | 컨트롤 묶기 + 제목 | Text |
Enter | grpSize |
| Panel | 컨트롤 묶기 + 스크롤 막대 | AutoScroll, BorderStyle |
Paint | pnlBottom |
| ListView | 여러 열로 된 목록(표 모양) | View, Columns, Items |
SelectedIndexChanged | lvHistory |
ListView는 다음 글의 통장 프로그램에서 거래내역 표로 씁니다. 나머지 12개는 이 글의 예제에 모두 나오고, 위 그림에도 이 12개를 담았습니다.
컨트롤별로 알아 둘 점
공식 개요 문서에서 초보에게 필요한 내용만 골랐습니다.
- Label은 포커스를 받지 못합니다. 그래서 Tab 키로 이동할 때 Label에는 멈추지 않습니다.
- TextBox는 기본으로 한 줄 입력입니다.
Multiline을true로 바꾸면 여러 줄을 입력할 수 있습니다. - RadioButton은 같은 묶음 안에서 하나를 고르면 나머지가 풀립니다. 묶음의 단위는 GroupBox나 Panel 같은 컨테이너입니다. 크기와 다른 종류의 라디오 버튼을 한 폼에 두려면 GroupBox로 나눕니다. CheckBox는 몇 개든 함께 켤 수 있습니다.
- ComboBox의 기본 모양(
DropDown)은 글자 입력 칸이 붙어 있어서 목록에 없는 글자를 칠 수 있습니다. 목록에서만 고르게 하려면DropDownStyle을DropDownList로 바꿉니다. - ComboBox와 ListBox의
SelectedIndex는 고른 항목의 번호입니다. 0부터 세고, 아무것도 고르지 않았으면 -1입니다. 항목은 디자이너의Items속성이나 코드의Items.Add로 넣습니다. - ListBox는 넣은 객체의
ToString()결과를 화면에 보여 줍니다. 앞선 글에서 재정의한ToString()이 여기서 쓰입니다. - NumericUpDown의 기본값은
Minimum0,Maximum100,Increment1입니다. 값의 형식이decimal이라int로 쓰려면(int)nudQty.Value처럼 바꿉니다. - DateTimePicker로 시간만 고르게 하려면
Format을Custom,CustomFormat을HH:mm으로 하고ShowUpDown을true로 합니다. - PictureBox의
SizeMode기본값은Normal(원래 크기, 넘치면 잘림)입니다.Zoom으로 바꾸면 비율을 지키며 상자에 맞춥니다. - GroupBox와 Panel은 둘 다 컨트롤을 묶습니다. 제목은 GroupBox만 보여 주고, 스크롤 막대는 Panel만 가질 수 있습니다.
공통 속성 다섯 개
| 속성 | 뜻 | 알아 둘 점 |
|---|---|---|
| Name | 코드에서 쓰는 변수 이름 | 컨트롤을 놓자마자, 이벤트를 만들기 전에 바꿉니다 |
| Text | 화면에 보이는 글자 | 버튼 글자, 레이블 내용, 폼 제목, GroupBox 제목. TextBox에서는 입력한 내용 |
| Anchor | 폼 크기가 바뀔 때 어느 가장자리와의 거리를 지킬지 | 기본값 Top, Left |
| Dock | 부모의 한쪽 면에 붙이기 | 기본값 None. Bottom, Fill 등 |
| TabIndex | Tab 키로 이동하는 순서 | 0부터, 놓은 순서대로 매겨짐 |
Anchor와 Dock은 창 크기를 바꿀 때의 동작을 정합니다. 공식 레이아웃 문서의 설명을 정리하면 아래와 같습니다.
- Anchor는 지정한 가장자리와의 거리를 지킵니다. 기본값
Top, Left이면 창을 늘려도 제자리에 있고,Bottom, Right이면 오른쪽 아래 모서리를 따라 움직이고, 네 방향 모두면 창과 함께 늘어납니다. - Dock은 부모의 한쪽 면에 붙고 그 방향으로 늘어납니다.
Bottom이면 아래에 붙어 폭이 부모만큼 늘어나고,Fill이면 남은 공간을 채웁니다. Dock한 컨트롤은 위치를 직접 정할 수 없습니다. - 여러 컨트롤을 Dock하면 앞뒤 순서에 따라 붙는 순서가 정해져 헷갈리기 쉽습니다. 처음에는 Dock 컨트롤을 하나만 쓰는 편이 쉽습니다.
TabIndex는 컨트롤을 놓은 순서대로 0부터 매겨집니다. GroupBox 같은 컨테이너 안의 컨트롤은 번호를 따로 매깁니다. 한 번에 바꾸려면 보기 > 탭 순서(Tab Order)로 탭 순서 모드에 들어가 원하는 순서대로 컨트롤을 클릭하고, 다시 보기 > 탭 순서를 눌러 끝냅니다. TabStop을 false로 하면 Tab 키가 그 컨트롤을 건너뜁니다.
이름 짓기 관례
컨트롤을 놓으면 VS가 button1, textBox1 같은 이름을 붙입니다. 코드에서 button3이 무엇인지 알 수 없으므로 놓자마자 Name을 바꿉니다. 공식 자습서도 lstNames, txtName, btnAdd처럼 종류를 나타내는 앞글자에 역할을 붙인 이름을 씁니다. 이 글은 btn, txt, lbl, chk, rdo, cbo, lst, nud, dtp, pic, grp, pnl, lv를 씁니다. 이 앞글자 목록은 공식 표준이 아니라 널리 쓰이는 관례이고, cmb나 rb처럼 다르게 쓰는 곳도 있습니다.
.NET 라이브러리 설계 지침에는 이름에 형식 표시를 붙이지 말라는(헝가리식 표기 금지) 규칙이 있습니다. 이 규칙은 다른 사람이 쓰는 라이브러리의 공개 이름에 대한 것이고, 폼 안의 private 컨트롤 변수에 앞글자를 붙이는 것은 WinForms에서 흔히 쓰는 방식입니다.
MessageBox, 코드로 띄우는 알림 창
MessageBox는 도구 상자의 컨트롤이 아니라 코드로 띄우는 알림 창입니다. MessageBox.Show는 여러 모양으로 부를 수 있고, 사용자가 누른 버튼을 DialogResult로 돌려줍니다. 아래는 세 가지 모양과 돌려받는 값을 쓰는 코드입니다. 앞의 FirstForm에서 Form1.cs만 바꿔 빌드를 확인했습니다.
namespace MessageBoxDemo;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// MessageBox.Show의 모양 세 가지와 돌려받는 값(DialogResult)
private void btnHello_Click(object sender, EventArgs e)
{
MessageBox.Show("저장했습니다."); // 내용만
MessageBox.Show("이름을 입력해 주세요.", "알림"); // 내용, 제목
MessageBox.Show("이름을 입력해 주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Warning); // + 버튼 종류, 아이콘
DialogResult answer = MessageBox.Show("주문할까요?", "주문 확인",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (answer == DialogResult.Yes)
{
lblResult.Text = "예를 눌렀습니다.";
}
else
{
lblResult.Text = "아니요를 눌렀습니다.";
}
}
}
빌드 성공 (경고 0개)
출력 폴더 bin\Debug\net10.0-windows\:
MessageBoxDemo.deps.json
MessageBoxDemo.dll
MessageBoxDemo.exe
MessageBoxDemo.pdb
MessageBoxDemo.runtimeconfig.json
MessageBoxButtons에는OK,OKCancel,YesNo,YesNoCancel등이 있고,MessageBoxIcon에는Information,Warning,Error,Question등이 있습니다.- 메시지 상자가 떠 있는 동안에는 뒤의 폼을 누를 수 없고, 사용자가 닫아야 다음 줄이 실행됩니다. 그래서
Show가 누른 버튼을 돌려줄 수 있습니다. - 버튼 글자(예/아니요)와 아이콘 모양은 Windows가 언어와 버전에 맞춰 그립니다. 실제 모양은 환경에 따라 다를 수 있습니다.
헷갈리기 쉬운 점
- RadioButton 묶음은 컨테이너로 나눕니다. 크기 라디오 버튼과 다른 라디오 버튼을 같은 폼 바닥에 두면 전체에서 하나만 선택됩니다. GroupBox에 나눠 넣습니다.
- ComboBox에 글자가 입력된다면 DropDownStyle을 확인합니다. 기본값
DropDown은 입력 칸이 있는 모양입니다. 목록에서만 고르게 하려면DropDownList로 바꿉니다. - NumericUpDown의 Value는 decimal입니다.
int count = nudQty.Value;처럼 바로 넣으면 decimal을 int로 자동 변환할 수 없다는 빌드 오류(CS0266)가 납니다.(int)nudQty.Value처럼 바꿔 씁니다. - Text를 바꿔도 Name은 그대로입니다. 버튼에 "확인"이라고 써도 코드에서는 여전히
button1입니다. Name을 따로 바꿉니다.
5. 이벤트 처리기

이벤트는 "버튼이 눌렸다", "글자가 바뀌었다"처럼 컨트롤에 일어난 일입니다. 이벤트 처리기는 그 일이 일어났을 때 실행될 메서드입니다. Windows Forms 프로그램은 대부분 이벤트 처리기를 채우는 것으로 만듭니다.
방법 1: 컨트롤 더블클릭
디자이너에서 컨트롤을 더블클릭하면 그 컨트롤의 기본 이벤트 처리기가 만들어지고 코드 화면으로 넘어갑니다. Button이면 Click, TextBox면 TextChanged, 폼이면 Load입니다. 기본 이벤트는 4장 표의 "더블클릭 이벤트" 열에 있습니다.
방법 2: 속성 창의 이벤트 목록
기본 이벤트가 아닌 이벤트를 쓰거나, 처리기 하나를 여러 컨트롤이 같이 쓰게 할 때는 속성 창을 씁니다.
- 디자이너에서 컨트롤을 고릅니다.
- 속성 창에서 이벤트(Events) 단추를 눌러 이벤트 목록으로 바꿉니다.
- 원하는 이벤트(예:
Click)를 찾습니다. - 이벤트 이름을 더블클릭하면 새 처리기가 만들어집니다. 이미 처리기가 있으면 그 코드로 갑니다.
- 이미 만든 처리기를 쓰려면 이벤트 이름 오른쪽의 드롭다운에서 메서드를 고릅니다. 목록에는 그 이벤트에 맞는 매개 변수를 가진 메서드만 나옵니다.
6장의 커피 주문 폼은 5번 방법으로 메뉴, 크기, 옵션, 수량 컨트롤 7개가 처리기 Option_Changed 하나를 같이 씁니다.
더블클릭 한 번에 바뀌는 두 파일
btnHello를 더블클릭하면 두 파일이 바뀝니다. 3장의 FirstForm 예제에서 해당 줄만 그대로 옮겼습니다. 먼저 Form1.cs에 처리기 메서드가 생깁니다. 처음에는 중괄호 안이 비어 있고, 안은 내가 채웁니다.
// ... (생략) ...
// 디자이너에서 btnHello를 더블클릭하면 만들어지는 Click 이벤트 처리기
private void btnHello_Click(object sender, EventArgs e)
{
// ... (생략) ...
}
동시에 Form1.Designer.cs의 InitializeComponent 안에 연결 줄이 생깁니다. 이 줄은 디자이너가 씁니다.
// ... (생략) ...
btnHello.Click += btnHello_Click;
- 처리기 이름은 VS가 "Name_이벤트"로 만듭니다. Name을 먼저 바꿔야 처리기 이름도
button1_Click이 아니라btnHello_Click이 됩니다. sender는 이벤트를 일으킨 컨트롤,e는 이벤트의 추가 정보입니다. 이 글의 예제에서는 둘 다 쓰지 않습니다.+=는 "이 이벤트가 일어나면 이 메서드를 실행"하도록 연결합니다. 옛 글에는this.btnHello.Click += new System.EventHandler(this.btnHello_Click);모양이 많은데 뜻은 같습니다. VS 버전에 따라 디자이너가 쓰는 모양이 다를 수 있습니다.
처리기를 지울 때 생기는 오류
필요 없는 처리기를 지우려고 Form1.cs에서 메서드만 지우면 빌드가 실패합니다. FirstForm의 Form1.cs에서 btnHello_Click 메서드를 지우고 빌드한 결과입니다.
Form1.Designer.cs(61,27): error CS0103: The name 'btnHello_Click' does not exist in the current context
오류가 지운 파일이 아니라 Form1.Designer.cs에서 납니다. 디자이너 파일에 남은 연결 줄 btnHello.Click += btnHello_Click;이 없는 메서드를 찾기 때문입니다. 공식 문서도 처리기 코드만 지우면 디자이너가 계속 참조한다고 안내합니다.
- 바른 방법은 속성 창의 이벤트 목록에서 그 이벤트를 오른쪽 클릭하고 다시 설정(Reset)을 고르는 것입니다.
- 이미 오류가 났다면 오류 줄을 더블클릭해
Form1.Designer.cs로 가서 연결 줄 한 줄만 지웁니다. 디자이너 파일을 손으로 고치는 거의 유일한 경우입니다.
Name을 나중에 바꿀 때도 비슷한 오류를 만날 수 있습니다. 아래는 Form1.cs는 그대로 두고 디자이너 파일의 txtName만 모두 txtUser로 바꿔 빌드한 결과입니다.
Form1.cs(13,23): error CS0103: The name 'txtName' does not exist in the current context
Form1.cs(18,13): error CS0103: The name 'txtName' does not exist in the current context
Form1.cs가 여전히 옛 이름 txtName을 쓰고 있어서 생긴 오류입니다. VS 속성 창에서 Name을 바꿀 때 코드의 사용처까지 함께 바뀌는지는 환경에 따라 다를 수 있습니다. 가장 안전한 순서는 공식 자습서처럼 "컨트롤 놓기, Name과 Text 바꾸기, 그다음 더블클릭"입니다.
헷갈리기 쉬운 점
- 실수로 더블클릭해서 생긴 처리기도 다시 설정으로 지웁니다. TextBox를 잘못 더블클릭하면
txtName_TextChanged가 생깁니다. 메서드만 지우지 말고 이벤트 목록에서 다시 설정을 고릅니다. - 오류 위치가 Designer 파일이어도 원인은 Form1.cs일 수 있습니다. 두 파일이 한 클래스라서 한쪽만 바뀌면 다른 쪽에서 오류가 납니다.
- 오류 설명은 VS 언어 설정에 따라 한국어로 나올 수 있습니다. 문구가 이 글과 달라도 오류 코드
CS0103은 같으니, 오류 코드로 찾습니다. - 처리기 메서드는 private이어도 됩니다. 같은 클래스(
partial class Form1) 안에서 연결하기 때문입니다.
6. 예제: 커피 주문 폼

지금까지 본 컨트롤을 한 폼에 모아 커피 주문 프로그램을 만듭니다. 메뉴를 ComboBox에서 고르고, 크기를 RadioButton으로, 옵션을 CheckBox로, 수량을 NumericUpDown으로 고르면 한 잔 가격과 금액이 바로 계산되도록 씁니다. 담기 버튼으로 주문 목록(ListBox)에 넣고, 주문하기 버튼을 누르면 확인 메시지 상자를 띄우도록 씁니다. 프로젝트 이름은 CoffeeOrder입니다.
폼 설계
아래는 컨트롤 배치를 글자로 그린 구조도입니다. 실제 창 모양이 아니고, 괄호 안은 Name입니다. 실제 화면의 간격과 크기는 다를 수 있습니다.
┌──────────────────────── 커피 주문 (Form1) ────────────────────────┐
│ 주문자 [txtCustomer ] [picCoffee] 주문 목록 │
│ 메뉴 [cboMenu ▼] ┌──────────────────┐ │
│ ┌ 크기 (grpSize) ┐ ┌ 옵션 (grpOption) ┐ │ lstOrders │ │
│ │ ◉ 작은 컵 │ │ ☐ 샷 추가 +500원 │ │ │ │
│ │ ○ 중간 컵 │ │ ☐ 시럽 추가 +300원 │ │ │ │
│ │ ○ 큰 컵 │ └───────────────────┘ └──────────────────┘ │
│ └────────────────┘ │
│ 수량 [nudQty] 픽업 [dtpPickup] [빼기 btnRemove] │
│ lblPrice: 1잔 4,500원 × 1 = 4,500원 [담기 btnAdd] │
├────────────────────── pnlBottom (Dock = Bottom) ──────────────────┤
│ lblTotal: 합계: 0원 [주문하기 btnOrder] │
└───────────────────────────────────────────────────────────────────┘
디자이너에서 바꾼 속성과 연결한 이벤트입니다. 글자만 적은 Label 5개(lblCustomer, lblMenu, lblQty, lblPickup, lblOrders)는 표에서 뺐습니다.
| Name | 종류 | 바꾼 속성 | 이벤트와 처리기 |
|---|---|---|---|
txtCustomer |
TextBox | – | – |
cboMenu |
ComboBox | DropDownStyle = DropDownList, Items 4개 |
SelectedIndexChanged → Option_Changed |
picCoffee |
PictureBox | BorderStyle = FixedSingle, SizeMode = Zoom, TabStop = false |
– |
grpSize |
GroupBox | Text = 크기 |
– |
rdoSmall, rdoMedium, rdoLarge |
RadioButton | rdoSmall만 Checked = true |
CheckedChanged → Option_Changed |
grpOption |
GroupBox | Text = 옵션 |
– |
chkShot, chkSyrup |
CheckBox | – | CheckedChanged → Option_Changed |
nudQty |
NumericUpDown | Minimum = 1, Maximum = 10, Value = 1 |
ValueChanged → Option_Changed |
dtpPickup |
DateTimePicker | Format = Custom, CustomFormat = HH:mm, ShowUpDown = true |
– |
lblPrice |
Label | – | – |
btnAdd |
Button | Text = 담기 |
Click → btnAdd_Click |
lstOrders |
ListBox | Anchor = 위·아래·왼쪽·오른쪽 |
– |
btnRemove |
Button | Text = 빼기, Anchor = 아래·오른쪽 |
Click → btnRemove_Click |
pnlBottom |
Panel | Dock = Bottom, BackColor = ControlLight |
– |
lblTotal |
Label | 글꼴 Segoe UI 11pt 굵게 | – |
btnOrder |
Button | Text = 주문하기, Anchor = 위·오른쪽 |
Click → btnOrder_Click |
Form1 |
Form | Text = 커피 주문, StartPosition = CenterScreen |
– |
lstOrders는 네 방향 Anchor라서 창을 키우면 목록도 함께 커지고, btnRemove는 오른쪽 아래 모서리를 따라 움직이고, pnlBottom은 창 아래에 붙어 있도록 정했습니다. 실제 화면 배치는 환경에 따라 다를 수 있습니다.
가격 계산 클래스
가격표와 계산은 폼과 상관없는 클래스로 뺐습니다. 화면 코드와 계산 코드를 나누면 계산이 맞는지 따로 확인하기 쉽습니다.
namespace CoffeeOrder;
// 화면(폼)과 상관없는 가격표와 계산. 계산 확인용 콘솔 프로젝트(CoffeePrice)도 이 파일을 그대로 씁니다.
public static class CoffeeMenu
{
public static readonly string[] MenuNames = { "아메리카노", "카페라테", "바닐라라테", "녹차라테" };
private static readonly int[] BasePrices = { 4500, 5000, 5500, 5300 };
public static readonly string[] SizeNames = { "작은 컵", "중간 컵", "큰 컵" };
private static readonly int[] SizeExtras = { 0, 500, 1000 };
public const int ShotPrice = 500;
public const int SyrupPrice = 300;
// 메뉴 이름으로 기본 가격 찾기 (없는 메뉴면 예외)
public static int BasePrice(string menu)
{
int index = Array.IndexOf(MenuNames, menu);
if (index < 0)
{
throw new ArgumentException($"없는 메뉴입니다: {menu}");
}
return BasePrices[index];
}
// 한 잔 가격 = 기본 가격 + 크기 추가 + 옵션
public static int UnitPrice(string menu, int sizeIndex, bool shot, bool syrup)
{
int price = BasePrice(menu) + SizeExtras[sizeIndex];
if (shot)
{
price += ShotPrice;
}
if (syrup)
{
price += SyrupPrice;
}
return price;
}
}
주문 목록의 한 줄은 OrderLine 클래스입니다. ListBox는 넣은 객체의 ToString() 결과를 보여 주므로 ToString()을 재정의해 목록에 보일 글자를 정합니다.
namespace CoffeeOrder;
// 주문 목록(ListBox)의 한 줄. ListBox는 ToString()의 결과를 화면에 보여 줍니다.
public class OrderLine
{
public string Menu { get; }
public int SizeIndex { get; }
public bool Shot { get; }
public bool Syrup { get; }
public int Quantity { get; }
public OrderLine(string menu, int sizeIndex, bool shot, bool syrup, int quantity)
{
Menu = menu;
SizeIndex = sizeIndex;
Shot = shot;
Syrup = syrup;
Quantity = quantity;
}
public int UnitPrice => CoffeeMenu.UnitPrice(Menu, SizeIndex, Shot, Syrup);
public int Total => UnitPrice * Quantity;
public override string ToString()
{
string options = "";
if (Shot)
{
options += " +샷";
}
if (Syrup)
{
options += " +시럽";
}
return $"{Menu}({CoffeeMenu.SizeNames[SizeIndex]}{options}) × {Quantity} = {Total:N0}원";
}
}
폼 코드
namespace CoffeeOrder;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
cboMenu.SelectedIndex = 0; // 첫 메뉴 선택 (이때 SelectedIndexChanged도 실행됨)
dtpPickup.Value = DateTime.Now.AddMinutes(10); // 픽업 시간 기본값: 10분 뒤
}
// 크기 라디오 버튼 3개 중 어느 것이 선택됐는지 → 0, 1, 2
private int SelectedSizeIndex()
{
if (rdoLarge.Checked)
{
return 2;
}
if (rdoMedium.Checked)
{
return 1;
}
return 0;
}
// 지금 화면에서 고른 값으로 주문 한 줄 만들기
private OrderLine CreateLine()
{
return new OrderLine(cboMenu.Text, SelectedSizeIndex(),
chkShot.Checked, chkSyrup.Checked, (int)nudQty.Value);
}
// 메뉴·크기·옵션·수량이 바뀔 때마다 실행 (여러 컨트롤의 이벤트가 이 처리기 하나를 같이 씀)
private void Option_Changed(object sender, EventArgs e)
{
if (cboMenu.SelectedIndex < 0)
{
return;
}
OrderLine line = CreateLine();
lblPrice.Text = $"1잔 {line.UnitPrice:N0}원 × {line.Quantity} = {line.Total:N0}원";
// 그림 대신 메뉴마다 배경색을 바꿔 PictureBox를 확인합니다
if (line.Menu == "아메리카노")
{
picCoffee.BackColor = Color.SaddleBrown;
}
else if (line.Menu == "녹차라테")
{
picCoffee.BackColor = Color.OliveDrab;
}
else
{
picCoffee.BackColor = Color.Tan;
}
}
private void btnAdd_Click(object sender, EventArgs e)
{
lstOrders.Items.Add(CreateLine()); // ListBox에는 OrderLine.ToString()이 보입니다
UpdateTotal();
}
private void btnRemove_Click(object sender, EventArgs e)
{
if (lstOrders.SelectedIndex < 0)
{
MessageBox.Show("뺄 항목을 목록에서 선택해 주세요.", "알림");
return;
}
lstOrders.Items.RemoveAt(lstOrders.SelectedIndex);
UpdateTotal();
}
private void btnOrder_Click(object sender, EventArgs e)
{
string customer = txtCustomer.Text.Trim();
if (customer == "")
{
MessageBox.Show("주문자 이름을 입력해 주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtCustomer.Focus();
return;
}
if (lstOrders.Items.Count == 0)
{
MessageBox.Show("담은 메뉴가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
string message = $"{customer}님, {lstOrders.Items.Count}건 {GetTotal():N0}원\n"
+ $"픽업 시간 {dtpPickup.Value:HH:mm}\n주문할까요?";
DialogResult answer = MessageBox.Show(message, "주문 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (answer == DialogResult.Yes)
{
MessageBox.Show("주문이 접수되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
lstOrders.Items.Clear();
UpdateTotal();
}
}
private int GetTotal()
{
int total = 0;
foreach (OrderLine line in lstOrders.Items)
{
total += line.Total;
}
return total;
}
private void UpdateTotal()
{
lblTotal.Text = $"합계: {GetTotal():N0}원";
}
}
- 생성자에서
cboMenu.SelectedIndex = 0;을 하면 그 순간SelectedIndexChanged가 일어나Option_Changed가 한 번 실행됩니다. 그래서 폼이 열릴 때 가격 줄이 채워지도록 했습니다.Option_Changed맨 앞의SelectedIndex < 0검사는 아무 메뉴도 고르지 않았을 때를 막습니다. Option_Changed는 메뉴, 크기, 옵션, 수량 중 무엇이 바뀌든 가격을 다시 계산합니다. 그래서 7개 컨트롤의 이벤트가 이 처리기 하나를 같이 씁니다. 그림 파일을 넣으면 설명이 길어지므로, PictureBox는 메뉴마다 배경색만 바꿉니다.btnAdd_Click은lstOrders.Items.Add(CreateLine())로OrderLine객체를 목록에 넣습니다. 목록에는 글자가 아니라 객체가 들어가므로,GetTotal에서foreach (OrderLine line in lstOrders.Items)로 다시 꺼내 금액을 더할 수 있습니다.btnOrder_Click은 주문자 이름과 목록을 검사한 뒤 예/아니요 메시지 상자를 띄우고, 돌려받은 값이DialogResult.Yes일 때만 주문을 접수하도록 썼습니다.- 지난 글의 콘솔 입력 검사가 폼에서는 "버튼을 누를 때 검사하고 MessageBox로 알리기"로 바뀌었습니다.
디자이너 코드
디자이너 파일은 371줄이라 모양을 보여 주는 줄만 그대로 옮겼습니다. 3장과 마찬가지로 VS가 만드는 모양을 따라 손으로 썼고, 위치·크기 숫자는 VS에서 끌어다 놓으면 다를 수 있습니다. 전체 파일은 디자이너가 만들어 주며, // ... (생략) ... 자리에는 나머지 컨트롤마다 같은 모양(만들기, 속성 블록, 연결 줄, 필드)이 반복됩니다. 직접 따라 할 때는 이 코드를 치지 말고 디자이너에서 컨트롤을 놓은 뒤 위 표대로 속성을 바꿉니다.
namespace CoffeeOrder;
partial class Form1
{
// ... (생략) ...
private void InitializeComponent()
{
// ... (생략) ...
btnAdd = new Button();
// ... (생략) ...
//
// btnAdd
//
btnAdd.Location = new Point(225, 232);
btnAdd.Name = "btnAdd";
btnAdd.Size = new Size(75, 23);
btnAdd.TabIndex = 12;
btnAdd.Text = "담기";
btnAdd.UseVisualStyleBackColor = true;
btnAdd.Click += btnAdd_Click;
// ... (생략) ...
//
// Form1
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(520, 330);
Controls.Add(pnlBottom);
Controls.Add(btnRemove);
Controls.Add(lstOrders);
Controls.Add(lblOrders);
Controls.Add(btnAdd);
// ... (생략) ...
Controls.Add(lblCustomer);
Name = "Form1";
StartPosition = FormStartPosition.CenterScreen;
Text = "커피 주문";
// ... (생략) ...
ResumeLayout(false);
PerformLayout();
}
// ... (생략) ...
private Button btnAdd;
private Label lblOrders;
private ListBox lstOrders;
// ... (생략) ...
}
btnAdd = new Button();으로 만들고,// btnAdd아래 블록에서 위치(Location), 크기(Size), 이름(Name), 글자(Text), 탭 순서(TabIndex)를 정합니다. 블록 끝의btnAdd.Click += btnAdd_Click;이 5장에서 본 연결 줄입니다.// Form1블록의Controls.Add가 컨트롤을 폼에 넣습니다. 맨 아래private Button btnAdd;같은 필드가Form1.cs에서 쓰는 컨트롤 변수입니다.- 생략한 부분에서 라디오 버튼 3개는
grpSize.Controls.Add로, 체크 상자 2개는grpOption.Controls.Add로 각 GroupBox 안에 들어갑니다. 그래서 GroupBox 안 컨트롤의 TabIndex는 0부터 따로 매겨집니다(grpSize는 5, 안의 라디오 버튼은 0, 1, 2). - 생략한 부분에는
Option_Changed를 같이 쓰는 연결 줄이 7개(cboMenu, 라디오 버튼 3개, 체크 상자 2개,nudQty) 있습니다. 모양은cboMenu.SelectedIndexChanged += Option_Changed;처럼btnAdd의 연결 줄과 같습니다.
Program.cs는 3장과 같고 namespace만 CoffeeOrder입니다. 빌드 결과입니다.
빌드 성공 (경고 0개)
출력 폴더 bin\Debug\net10.0-windows\:
CoffeeOrder.deps.json
CoffeeOrder.dll
CoffeeOrder.exe
CoffeeOrder.pdb
CoffeeOrder.runtimeconfig.json
계산 부분을 콘솔로 확인하기
폼을 띄우기 전에 가격 계산이 맞는지 콘솔에서 먼저 확인합니다. 계산 파일 CoffeeMenu.cs와 OrderLine.cs를 복사하지 않고 콘솔 프로젝트에 연결해 실행했습니다. 콘솔 프로젝트 파일에 <Compile Include="(폼 프로젝트 경로)\CoffeeMenu.cs" Link="CoffeeMenu.cs" /> 같은 줄을 넣으면 다른 프로젝트의 파일을 그대로 가져와 씁니다.
// 커피 주문 폼의 계산 부분만 콘솔에서 먼저 확인합니다 (폼을 띄우기 전에 가격 계산이 맞는지 보기)
using CoffeeOrder;
Console.WriteLine("=== 기본 가격(작은 컵, 옵션 없음) ===");
foreach (string menu in CoffeeMenu.MenuNames)
{
Console.WriteLine($"{CoffeeMenu.UnitPrice(menu, 0, false, false),7:N0}원 {menu}");
}
Console.WriteLine();
Console.WriteLine("=== 주문 목록(ListBox에 보일 글자 = ToString) ===");
List<OrderLine> orders = new List<OrderLine>
{
new OrderLine("아메리카노", 0, false, false, 2), // 작은 컵 2잔
new OrderLine("카페라테", 2, true, false, 1), // 큰 컵 + 샷
new OrderLine("녹차라테", 1, false, true, 3), // 중간 컵 + 시럽 3잔
};
int total = 0;
foreach (OrderLine line in orders)
{
Console.WriteLine(line);
total += line.Total;
}
Console.WriteLine($"합계: {total:N0}원");
Console.WriteLine();
Console.WriteLine("=== 없는 메뉴 이름 ===");
try
{
CoffeeMenu.UnitPrice("카푸치노", 0, false, false);
}
catch (ArgumentException ex)
{
Console.WriteLine($"ArgumentException: {ex.Message}");
}
실행 결과입니다. 가운데 주문 목록 부분이 폼의 ListBox에 보이도록 만든 글자입니다. 폼 화면이 아니라 콘솔 출력입니다.
=== 기본 가격(작은 컵, 옵션 없음) ===
4,500원 아메리카노
5,000원 카페라테
5,500원 바닐라라테
5,300원 녹차라테
=== 주문 목록(ListBox에 보일 글자 = ToString) ===
아메리카노(작은 컵) × 2 = 9,000원
카페라테(큰 컵 +샷) × 1 = 6,500원
녹차라테(중간 컵 +시럽) × 3 = 18,300원
합계: 33,800원
=== 없는 메뉴 이름 ===
ArgumentException: 없는 메뉴입니다: 카푸치노
카페라테 큰 컵에 샷을 더하면 5,000 + 1,000 + 500 = 6,500원이고, 녹차라테 중간 컵에 시럽을 더하면 5,300 + 500 + 300 = 6,100원이라 3잔은 18,300원입니다. 손으로 계산한 값과 맞습니다.
헷갈리기 쉬운 점
- 생성자에서 값을 바꿔도 이벤트가 일어납니다.
cboMenu.SelectedIndex = 0;한 줄에Option_Changed가 실행됩니다. 처리기 안에서 아직 준비되지 않은 컨트롤을 쓰면 문제가 생길 수 있어 맨 앞에서 검사합니다. - 라디오 버튼은 바뀔 때 이벤트가 두 번 일어날 수 있습니다. 하나가 켜지면서 다른 하나가 꺼지기 때문입니다. 이 예제는 가격을 다시 계산할 뿐이라 두 번 실행돼도 결과가 같습니다.
- 처리기 이름을 직접 지을 수 있습니다.
Option_Changed는 VS가 지은 이름이 아닙니다. 이벤트 목록의 이벤트 칸에 이름을 입력하는 방법은 VS 버전에 따라 다를 수 있으니, 더블클릭으로 만든 뒤 이름을 바꾸고 나머지 컨트롤은 드롭다운에서 고르는 방법도 있습니다. - ListBox의 SelectedIndex는 고르지 않으면 -1입니다.
btnRemove_Click처럼 -1인지 먼저 확인하지 않고RemoveAt을 부르면 예외가 납니다.
마치며
이번 글에서는 Windows Forms 앱의 기본을 정리했습니다.
- 템플릿은 「Windows Forms 앱」(.NET)을 고르고, 프로젝트 파일은
WinExe,net10.0-windows,UseWindowsForms입니다. - 폼 디자이너, 도구 상자, 속성 창, 솔루션 탐색기로 폼을 그립니다. 도구 상자와 속성 창은 보기 메뉴에서 엽니다.
Form1클래스는partial로Form1.cs(내가 쓰는 코드)와Form1.Designer.cs(디자이너가 쓰는 코드)에 나뉩니다. 디자이너 파일은 직접 고치지 않습니다.- 자주 쓰는 컨트롤 13종과 공통 속성 Name, Text, Anchor, Dock, TabIndex를 봤습니다. Name은 놓자마자 바꿉니다.
- 컨트롤을 더블클릭하거나 속성 창 이벤트 목록에서 처리기를 만듭니다. 처리기는 메서드만 지우지 말고 다시 설정으로 지웁니다.
- 커피 주문 폼을 만들고, 계산 부분은 콘솔로 실행해 확인했습니다.
Visual Studio에서 직접 실행해 보면 컨트롤 간격이나 메시지 상자 모양이 이 글의 그림과 조금 다를 수 있습니다.
다음 글은 「Windows Forms 통장 프로그램, 입금·출금·거래내역 만들기」입니다. 이번 글의 컨트롤과 이벤트 처리기로 입금과 출금을 하고, 거래내역을 ListView 표로 보여 주는 통장 프로그램을 만듭니다. 금액 입력 검사와 클래스 설계도 함께 다룹니다.
참고 자료
- Visual Studio에서 Windows Forms 앱 만들기(자습서)
- Windows Forms 개요
- NETSDK1136 오류
- Windows Forms 디자이너란?
- 도구 상자(Toolbox)
- 속성 창(Properties window)
- Visual Studio 기본 바로 가기 키
- .NET 6의 Windows Forms 새 기능(ApplicationConfiguration)
- Windows Forms 컨트롤 개요
- 기능별 Windows Forms 컨트롤
- 컨트롤 레이아웃 옵션(Anchor, Dock)
- 탭 순서 설정
- Enter 키 버튼 지정(AcceptButton)
- 컨트롤 이벤트 처리기 추가와 제거
- Windows Forms 이벤트 개요
- MessageBox.Show 메서드
- ListControl.DisplayMember 속성(ToString 표시)
- 일반 명명 규칙(헝가리식 표기)
'C#' 카테고리의 다른 글
| Windows Forms 그래프, ScottPlot으로 차트 그리기 (0) | 2026.10.06 |
|---|---|
| Windows Forms 통장 프로그램, 입금·출금·거래내역 만들기 (0) | 2026.10.06 |
| C# 콘솔 입출력, ReadLine과 출력 서식 (0) | 2026.10.06 |
| Visual Studio 콘솔앱 만들기, 파일 나누기와 디버깅 (0) | 2026.10.06 |
| C# 고급 문법, 클래스부터 LINQ까지 (0) | 2026.10.06 |