본문 바로가기

.Net Technology/WPF

(3) WPF 애플리케이션 만들기

Window의 부모 클래스들은 많은 기능들을 제공해주고 있기 때문에 Window 클래스를 바로 이용하는 것도 가능하고 아니면 상속받아 새로운 윈도우를 만드는 것도 가능하다. 그럼 그 두 가지 방식을 코드를 통해서 살펴보도록 하겠다. 대부분의 WPF 애플리케이션들이 XAML을 이용해서 윈도우를 만들긴 하지만 이것은 단지 선택적인 것이다. XAML로 할 수 있는 것들은 모두 코드를 이용해서 만들 구현하는 것이 가능하다. 즉, 객체지향의 기반으로 WPF 프로그램을 만드는 것이 가능하다는 것이다.

그럼 예제를 만들어 확인해 보도록 하자. 여기서 우리는 XAML을 이용하지 않고 Application 클래스와 Window 클래스를 이용해서 코드를 작성해 볼 것이고 SimpleWPFApp.cs 파일과 C#코드를 이용해서 적당한 매인창을 만들어 보도록 하겠다.

// 코드를 이용해서 간단한 WPF 애플리케이션 만들기
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleWPFApp
{
  //첫 번째 예제에서 하나의 클래스를 정의하고 매인 윈도우로 띄워 보여준다.
  class MyWPFApp : Application
  {
    [STAThread]
    static void Main()
    {
      // Startup 이벤트를 선언하고 애플리케이션을 실행한다. 
      MyWPFApp app = new MyWPFApp();
      app.Startup += AppStartUp;
      app.Exit += AppExit;
      app.Run(); // Startup 이벤트가 발생된다.
    }
    static void AppExit(object sender, ExitEventArgs e)
    {
      MessageBox.Show("App has exited");
    }
    static void AppStartUp(object sender, StartupEventArgs e)
    {
      // Window객체를 생성하고 몇 가지 설정을 추가한다. 
      Window mainWindow = new Window();
      mainWindow.Title = "My First WPF App!";
      mainWindow.Height = 200;
      mainWindow.Width = 300;
      mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
      mainWindow.Show();
    }
  }
}



WPF 애플리케이션의 Main() 메서드에는 다른 COM 객체와 안전한 스레드 운영을 위해서 반드시 [STAThread]를 추가해주어야 한다. 만약 추가하지 않는다면 런타임시 예외가 발생하게 될 것이다.

MyWPFApp는 System.Windows.Application에서 확장된 클래스이다. 이 클래스의 Main() 메서드에서는 애플리케이션을 생성하고 Startup과 Exit이벤트를 설정해 주었다. 11장에서는 이벤트를 수동으로 빠르게 생성하는 방법에 대해서 살펴봤었다.
만약 델리게이트를 직접 지정해 주고 싶다면 다음 코드처럼 원하는 이름대로 지정하는 것도 가능하다. 하지만 Startup이벤트에 등록할 StartupEventHandler의 델리게이트의 메서드는 반드시 첫 번째 파라메터는 Object 객체를, 두 번째 파라메터로 StartupEventArgs 타입을 제공하고 있어야 한다. Exit 이벤트에 등록할 ExitEventHandler 델리게이트의 메서드는 두 번째 파라메터로 ExitEventArgs 타입을 제공해야 한다.

[STAThread]
static void Main()
{
    // 델리게이트 기반으로 생성
    MyWPFApp app = new MyWPFApp();
    app.Startup += new StartupEventHandler(AppStartUp);
    app.Exit += new ExitEventHandler(AppExit);
    app.Run(); // Startup 이벤트가 발생된다.
}



AppStartUp 메서드에서는 몇 개의 기본 속성을 설정한 Window 클래스를 생성하고 Show() 메서드를 호출한다. 이 때 창은 모달이 아닌 일반 창으로 화면에 보여지게 될 것이다. 그리고 AppExit() 메서드는 간단하게 종료한다는 메시지를 보여주는 대화상자를 띄워주게 된다.
C# 코드를 컴파일하기 위해서 WPF 어셈블리들을 참조하는 build.rsp라는 이름의 파일을 만들어야 한다고 추정할 수 있을 것이다. 각각의 어셈블리 경로는 한 줄로 정의된다는 것을 알아두자.(우리는 2장에서 명령 프롬포트를 이용해서 컴파일이 가능하다.)

# build.rsp
#
/target:winexe
/out:SimpleWPFApp.exe
/r:"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll"
/r:"C:\Program Files\Reference Assemblies\Microsoft\Framework
\v3.0\PresentationCore.dll"
1012 CHAPTER 28 n INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML
8849CH28.qxd 10/19/07 9:28 AM Page 1012
/r:"C:\Program Files\Reference Assemblies\Microsoft\Framework
\v3.0\PresentationFramework.dll"
*.cs


명령 프롬포트에 다음과 같은 명령줄을 이용해서 컴파일 할 수 있다. 

csc @build.rsp

한번 이렇게 실행해보면 최소화, 최대화 그리고 닫기가 가능한 간단한 창이 열리는 것을 확인할 수 있을 것이다. 여기에 몇 가지 사용자 인터페이스 요소를 추가해 볼 수 있다. 그전에 먼저 Window 창을 상속받아 윈도우 창을 확장하는 예제를 살펴보도록 하겠다.

Window 클래스의 확장

일반적으로 WPF 애플리케이션을 만들 때는 Window 클래스를 상속받아서 만들게 된다. 즉, Window에서 제공되는 기능들을 기본적으로 사용하면서 필요한 기능은 확장해서 사용하게 되는 것이다. 앞에서 만들었던 SimpleWPFApp 네임스페이스 안에 다음과 같은 코드를 작성해 보도록 하자. 

class MainWindow : Window
{
    public MainWindow(string windowTitle, int height, int width)
    {
        this.Title = windowTitle;
        this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
        this.Height = height;
        this.Width = width;
        this.Show();
    }
}



이 코드에서 Startup 이벤트를 지정해 보도록 하자. 

static void AppStartUp(object sender, StartupEventArgs e)
{
    // MainWindow 객체 만들기
    MainWindow wnd = new MainWindow("My better WPF App!", 200, 300);
}


이 프로그램을 재 컴파일 해서 다시 실행해 보면 동일한 화면이 열릴 것이다. 이러한 프로그래밍 모델의 장점은 기능들을 더 확장할 수 있다는 것이다. 

Window 객체를 만들 경우 자동적으로 Application 클래스에 추가 될 것이다. (생성자에서 Application.Windows 객체를 찾아 추가된다.) 이렇게 추가된 Window객체는 프로그램이 종료되거나 Application.Windows 속성에 명시적으로 제거하기 전까지는 메모리에 존재한다.


간단한 UI 만들기

Window를 상속받은 클래스에 추가할 UI 요소들은 System.Windows.Forms.Form에 UI를 추가하는 것과 거의 비슷하다. 

1. 보여줄 멤버 변수를 정의한다. 
2. Window클래스 안에서 보여질 UI의 모양을 설정한다.
3. AddChild() 메서드 호출을 이용해서 Windows 영역에 추가한다. 

이렇게 UI를 추가하는 단계가 윈도우 폼 개발과 비슷하긴 하지만 확실히 다른 부분은 UI컨트롤이 System.Windows.Forms 이 아닌 System.Windows.Controls 안에 정의되어 있다는 것이다. 그래도 다행인 부분은 윈도우 폼과 이름이나 느낌이 상당히 비슷하다는 것이다. 
그리고 윈도우 폼과 다른 부분은 Window를 상속받은 폼은 단 하나의 자식요소만 가질 수 있다는 것이다. 윈도우에는 다양한 UI 요소를 보여주고 싶을 경우에는 먼저 DockPanel, Grid, Canvas, StackPanel와 같은 레이아웃 패널을 이용해야만 한다. 

그럼 예제를 만들어 보도록 하자. 먼저 우리는 하나의 버튼을 생성할 것이다. 그리고 이 버튼이 클릭되었을 때 우리는 Application 객체의 Shutdown() 메서드를 호출해서 애플리케이션을 종료 하게 설정할 것이다. MainWindow 클래스를 다음과 같이 업데이트 해보도록 하자.

class MainWindow : Window
{
    // UI  객체
    private Button btnExitApp = new Button();
    public MainWindow(string windowTitle, int height, int width)
    {
        // 버튼의 설정
        btnExitApp.Click += new RoutedEventHandler(btnExitApp_Clicked);
        btnExitApp.Content = "Exit Application";
        btnExitApp.Height = 25;
        btnExitApp.Width = 100;
        // 버튼추가
        this.AddChild(btnExitApp);
        // Window의 설정
        this.Title = windowTitle;
        this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
        this.Height = height;
        this.Width = width;
        this.Show();
    }
    private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
    {
        //이벤트가 발생하면 종료
        Application.Current.Shutdown();
    }
}



27장의 윈도우에서 살펴 본 것처럼 생성자에서의 코드는 그렇게 어려운 부분이 없을 것이다. 하지만 버튼을 눌렀을 때의 Click 이벤트에 RoutedEventHandler라는 델리게이트를 설정하였고 이 부분에 대해서 어떻게 이벤트를 설정한 것인지 궁금할 수 있다. 이 코드의 부분은 다음 장에서 자세히 살펴 볼 것이다. 여기서 간단히 설명하자면 RoutedEventHandler 델리게이트는 첫 번째 파라메터로 Object를 두 번째 파라메터로 RoutedEventArgs를 대입해주어야 한다는 것을 알아두자.

이렇게 코드를 만들어 재 컴파일하고 애플리케이션을 실행하면 다음 [그림4]과 같은 화면을 볼 수 있을 것이다. 여기서 버튼에 위치를 지정하지 않을 경우 가운데에 위치된다는 것을 알아두자. 
 


[그림4] WPF 애플리케이션


Application 클래스의 추가적인 설명

지금까지 WPF 프로젝트를 100% 순수한 코드를 이용해서 만들어 봤었다. 여기서는 전역적인 데이터를 관리할 수 있는 Application 클래스에 대해서 설명해 보도록 하겠다. 

Application 전역 데이터와 명령 파라메터의 처리

Application의 Properties라는 컬렉션 속성에 대해서 다시 한번 생각해 보도록 하자. 이 속성은 key/value 과 같은 컬렉션을 가지고 있고 값은 System.Object 타입으로 정의되어 있다. 때문에 어떠한 값을 저장할 수 있고 또한 쉽게 찾아서 사용할 수 있다. 그리고 이 속성에 저장된 값은 WPF 애플리케이션의 모든 창에서 공유가 가능하다. 

이에 대한 예제로 커맨드 라인에 /GODMODE 라는 값을 전달하고 Startup에서 이 값을 체크해 보도록 하겠다.(일반적으로 게임에서 치트키를 이용하기 위해서 주로 사용한다.) 만약 이러한 값을 찾아내면 특정 Properties의 속성에 COMMODE와 같은 이름으로 true 값을 설정해 보도록 하자. 만약 이 값이 없다면 false가 설정될 것이다. 

굉장히 간단한 예제이지만 어떻게 명령 파라메터를 가져올 것인지 궁금할 수 있을 것이다. 여러 방법 중에 하나는 Environment.GetCommandLineArgs()라는 스태틱 메서드를 호출하는 것이다. 하지만 이러한 파라메터는 자동적으로 StartupEventArgs 타입으로 Args 속성으로 넘어오게 된다. 그럼 다음과 같이 코드를 수정해 보도록 하자.

static void AppStartUp(object sender, StartupEventArgs e)
{
    // 명령 파라메터가 있는지 확인하고 만약 있다면 true를 설정한다.
    Application.Current.Properties["GodMode"] = false;
    foreach(string arg in e.Args)
    {
        if (arg.ToLower() == "/godmode")
        {
            Application.Current.Properties["GodMode"] = true;
            break;
        }
    }
    // MainWindow객체를 만든다.
    MainWindow wnd = new MainWindow("My better WPF App!", 200, 300);
}



이렇게 코드를 작성하면 어떤 WPF 애플리케이션에서든 이 값을 가져와 사용할 수 있다. 이 값을 가져오고 싶다면 Application.Current 객체를 이용해서 컬렉션을 조회하면 된다. 예를 들어 버튼의 클릭 이벤트가 발생할 때 값을 가져오는 코드는 다음과 같다. 

private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
{
    // /godmode 값을 넘겼는지 확인
    if((bool)Application.Current.Properties["GodMode"])
    MessageBox.Show("Cheater!");
    // 값을 가져온 후에 프로그램 종료
    Application.Current.Shutdown();
}



그리고 다음과 같은 메시지를 이용해서 호출해 보도록 하자. 

SimpleWPFApp.exe /godmode

이렇게 프로그램을 실행했다면 메시지가 뜬 다음에 프로그램이 종료되는 것을 볼 수 있을 것이다.

Application의 Windows 컬렉션 Iterating

더 흥미로운 부분은 Application 변수의 속성은 Windows 라는 속성을 제공하고 있고, 이 속성은 현재 WPF 애플리케이션 메모리에 로드 되어 있는 컬렉션들을 제공해주고 있다는 것이다. Window 클래스를 생성하면 자동으로 Windows 컬렌션에 추가 되기 때문에 우리는 특별한 작업을 하지 않아도 된다. 다음 예제는 모든 창을 최소화 하는 예제를 보여주고 있다. 

static void MinimizeAllWindows()
{
    foreach (Window wnd in Application.Current.Windows)
    {
        wnd.WindowState = WindowState.Minimized;
    }
}




Application의 추가적인 이벤트들

닷넷의 기본 클래스 라이브러리들 안의 많은 클래스들처럼 Application은 지정한 이벤트들을 가로채는 것이 가능하다. 그 예로 이미 Startup과 Exit 이벤트들을 살펴 보았다. 그리고 추가적으로 Activated 이벤트와 Deactivated 이벤트를 알고 있어야 한다. 이 이벤트를 얼핏 보면 Window 클래스에서 제공하고 있는 메서드 이름과 비슷하기 때문에 혼란스러울 수 있을 것이다. 하지만 UI 부분과는 다르게 Activated, Deactivated 이벤트들은 Application가 가지고 있는 어떤 Window라도 포커스를 잃거나 할 경우에 발생하게 된다. 

이 두 이벤트들은 여기서 예제로 다루지는 않을 것이다. 대신 간단히 이 이벤트에 대해서 설명하자면 이 이벤트는 System.EventHandler 델리게이터로 동작되고 첫 번째 파라메터로 Object를 그리고 두 번째 파라메터로 System.EventArgs를 받게 된다. (27장에서 EventHandler에 대한 델리게이트를 살펴봤었다.)


이 밖의 네비게이션 기반의 WPF 프로그램에서 사용하는 Application의 주요 이벤트들이 있다. 이 이벤트들은 객체끼리 이동할 때의 이벤트들을 가로채서 처리하는 것이 가능하다.


추가적인 Window 클래스의 내용

앞에서 살펴보았듯이 Window 클래스는 부모 클래스들과 구현된 인터페이스들의 기능을 이용할 수 있다. 이번에는 이러한 기본 클래스들의 여러 가지 기능들에 대해서 살펴볼 것이다. 여기서 설명할 Window 클래스의 추가적인 기능들은 굉장히 중요하고 또한 굉장히 많이 사용할 기능들이 많이 있을 것이다 그럼 먼저 생명주기에 대한 내용을 살펴보도록 하자.


Window 객체의 생명주기

System.Windows.Forms.Form 클래스처럼 System.Windows.Window 클래스는 생명 시간에 대한 이벤트를 가지고 있다. 몇 가지 이벤트들은 상당히 유용한 기능을 제공해줄 것이다. 무엇보다도 Window 클래스는 지정한 생성자를 호출하면 초기화 작업이 이루어지게 된다. 여기서 중요한 부분은 첫 번째 이벤트로 SourceInitialized 이벤트가 발생하게 된다는 것이다. 이 이벤트는 다양한 레거시 프로그램들과의 상호 운용성을 지원하기 위해서 만들어진 이벤트이다. 이 이벤트에 대한 자세한 설명은 닷넷 3.5에서 제공하는 문서에 대한 설명을 참조하기 바란다. 
그리고 생성자가 만들어진 다음에 System.EventHandler 델리게이트 기반으로 동작되는 Activate라는 이벤트가 발생하게 되고, 또한 창이 포커스를 가지게 되면 발생된다. 이와 반대로 Deactivate라는 이벤트가 존재하고 이 이벤트는 창에서 포커스를 잃었을 때 발생하게 된다. 
앞에서 Window를 상속받아 만들었던 클래스에 특정 메시지를 보여주는 private string 변수를 생성해서 Activate와 Deactivate 이벤트가 발생할 때 마다 이 변수에 값을 업데이트 해주도록 코드를 추가해 보도록 하자. 


class MainWindow : Window
{
    private Button btnExitApp = new Button();
    // 이벤트가 발생할 때마다 업데이트 되는 문자열
    private string lifeTimeData = String.Empty;

    protected void MainWindow_Activated(object sender, EventArgs e)
    {
        lifeTimeData += "Activate Event Fired!\n";
    }

    protected void MainWindow_Deactivated(object sender, EventArgs e)
    {
        lifeTimeData += "Deactivated Event Fired!\n";
    }
    
    public MainWindow(string windowTitle, int height, int width)
    {
        // 이벤트 설정
        this.Activated += MainWindow_Activated;
        this.Deactivated += MainWindow_Deactivated;
        ...
    }
}



 
Application에서 제공하는 Application.Activated와 Application.Deactivated 이벤트들은 모든 창들이 활성화 되고 비 활성화 될 때를 가로챌 수 있다. 

Activated 이벤트가 발생된 이후에는 RoutedEventHandler라는 델리게이트 기반으로 동작되는 Loaded 라는 이벤트가 발생된다. 이 이벤트는 렌더링이 완전히 끝난 이후에 자의 응답을 처리할 수 있는 상태가 된 후에 발생하게 된다. 

Activated는 포커스에 따라서 여러 번 이벤트가 발생할 수 있지만 Loaded 이벤트는 생명주기에 있어서 단 한번만 발생하게 된다는 것을 알아두자.

MainWindow 클래스에서 다음과 같은 코드를 추가하자. 

protected void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    lifeTimeData += "Loaded Event Fired!\n";
}



추가적으로 정확하게 렌더링에 대해서 확인하고 싶다면 ContentRendered 이벤트를 이용할 수 있다. 이 이벤트는 렌더링 되어 사용자와 상호 작용할 준비가 되면 발생하게 된다.


Window창이 닫칠 때의 동작

사용자는 다양한 방법으로 프로그램을 종료할 수 있다. 예를 들어 X 버튼을 눌러서 종료하거나 아니면 File 메뉴의 Exit와 같은 버튼을 눌러서 내부적으로 Close() 메서드를 호출해서 종료할 수도 있다. 이때 WPF는 사용자가 창을 종료한 후에 메모리에서 제거할지에 대해서 결정할 수 있게 해주는 두 가지 이벤트를 제공해주고 있다. 그 첫 번째가 바로 Closing 이벤트이고 이 이벤트는 CancelEventHandler 기반으로 동작된다. 

이 델리게이트는 System.ComponentModel.CancelEventArgs을 두 번째 파라메터로 받게 된다. CancelEventArgs는 Cancel이라는 속성을 제공하고 만약 이 값을 true로 설정하면 실제로 창이 닫치는 것을 막을 수 있다. (예를 들어 모르고 창을 닫았을 경우를 방지하거나 할 경우가 해당된다.)

만약 사용자가 실제로 창을 종료하고 싶다면 CancelEventArgs.Cancel 속성을 false로 설정할 수 있고 그렇게 종료된 후에는 Closed 이벤트가 발생하게 된다. (이 이벤트는 System.EventHandler 델리게이트 기반으로 동작된다.) MainWindow 클래스에서 이 두 가지 이벤트들을 설정해 보도록 하자. 

System.ComponentModel.CancelEventArgs e)
{
    lifeTimeData += "Closing Event Fired!\n";
    // 만약 창을 종료할 때의 메시지
    string msg = "Do you want to close without saving?";
    MessageBoxResult result = MessageBox.Show(msg,
    "My App", MessageBoxButton.YesNo, MessageBoxImage.Warning);
    if (result == MessageBoxResult.No)
    {
        // 사용자가 원하지 않을 경우
        e.Cancel = true;
    }
}

protected void MainWindow_Closed(object sender, EventArgs e)
{
    lifeTimeData += "Closing Event Fired!\n";
    MessageBox.Show(lifeTimeData);
}


이렇게 코드를 작성하고 컴파일을 한 후에 애플리케이션을 실행한 후에 포커스와 같은 작업을 발생시켜 보자. 그리고 한두 번 윈도우를 종료하려 시도한 후에 윈도우 창을 닫아 보도록 하자. 그럼 화면과 같은 메시지를 볼 수 있을 것이다. 이 메시지에서는 윈도우 창의 생명주기에 대한 이벤트를 확인할 수 있을 것이다. 
 

[그림5] System.Windows.Window의 생명주기


Window 창의 마우스 이벤트 다루기

윈도우 폼과 매우 비슷하게 WPF API는 여러 개의 마우스 이벤트들을 제공해주고 있다. 특히 UIElement라는 기본 클래스는 MouseMove, MouseUp, MouseDown, MouseEnter, MouseLeave와 같은 여러 개의 중요한 마우스 이벤트들을 정의하고 있다. 

예를 들어서 MouseMove 이벤트를 다룬다고 생각해보자. 이 이벤트는 System.Windows.Input.MouseEventHandler 델리게이트 기반으로 동작되고 이 델리게이트의 두 번째 파라메터로 System.Windows.Input.MouseEventArgs를 받게 된다. 윈도우 폼 애플리케이션과 같이 MouseEventArgs를 이용하면 현재 마우스의 X, Y 포지션을 가져올 수 있다. 코드를 살펴보면 다음과 같이 정의하고 있는 것을 볼 수 있다. 

public class MouseEventArgs : InputEventArgs
{
    ...
    public Point GetPosition(IInputElement relativeTo);
    public MouseButtonState LeftButton { get; }
    public MouseButtonState MiddleButton { get; }
    public MouseDevice MouseDevice { get; }
    public MouseButtonState RightButton { get; }
    public StylusDevice StylusDevice { get; }
    public MouseButtonState XButton1 { get; }
    public MouseButtonState XButton2 { get; }
}



GetPosition() 메서드를 이용하면 현재 Window에 위차한 UI 개체의 (x, y) 좌표를 가져올 수 있다. 즉, 현재 활성화된 창에서의 위치를 가져오고 싶다면 GetPosition()을 이용해서 쉽게 가져올 수 있다는 것이다. 다음 코드는 MouseMove 이벤트가 발생했을 때 마우스의 위치를 윈도우의 제목 표시줄에 보여주게 설정한 코드이다.

protected void MainWindow_MouseMove(object sender,
System.Windows.Input.MouseEventArgs e)
{
    // 마우스의 X,Y 좌표를 가져온다.
    this.Title = e.GetPosition(this).ToString();
}




키보드 이벤트 다루기

키보드 입력처리 또한 매우 간단하다. UIElement 클래스는 키보드와 관련된 동작들을 처리하는 여러 키보드 이벤트들을 지원하고 있다. (KeyUp, KeyDown 등) KeyUp와 KeyDown 이벤트들은 System.Windows.Input.KeyEventHandler 델리게이트 기반으로 동작되고 이 델리게이트의 두 번째 파라메터로 KeyEventArgs를 받게 된다. 이 타입은 다음과 같은 속성들을 가지고 있다. 

public class KeyEventArgs : KeyboardEventArgs
{
    ...
    public bool IsDown { get; }
    public bool IsRepeat { get; }
    public bool IsToggled { get; }
    public bool IsUp { get; }
    public Key Key { get; }
    public KeyStates KeyStates { get; }
    public Key SystemKey { get; }
}



다음 코드는 KeyUp 이벤트가 발생했을 때 눌러진 키를 제목표시줄에 보여주는 코드이다. 

protected void MainWindow_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
    // Display keypress.
    this.Title = e.Key.ToString();
}



이번 장을 살펴보면서 새로운 GUI 모델이란 WPF가 기존의 System.Windows.Forms.dll와 크게 다르지 않다고 느꼈을 수도 있을 것이다. 하지만 WPF로 어떤 것을 만들 수 있는지 보게 된다면 확실히 생각이 바뀌게 될 것이다. 그리고 WPF는 XAML이란 새로운 문법이 등장하게 되었고 XAML에 대해서 살펴보도록 하자. 



이 내용은 C#.NET Platform 원서의 내용을 기반으로 작성되었습니다.