스타일 그리고 템플릿(Style, Template) – {1/3}

WPF의 스타일과 템플릿 기능을 이용하면 어플리케이션의 외향을 고급스럽고 세련되게 바꿀 수 있고, 사용자에게 좀더 효과적으로 시스템을 이해하고 활용할 수 있는 환경을 제공할수 있다.

먼저 스타일과 템플릿이 전혀 적용되지 않는 것부터 시작해서 단계적으로 하나 하나 적용해 가면서 어떻게 어플리케이션의 외향이 고급스럽게 바뀌어 가는지를 살펴봄으로써 WPF의 스타일과 템플릿을 이해해 보도록 하겠다.

아래의 코드가 처음 단계에 대한 코드이며 이어지는 이미지가 실행결과이다.


  
  
    
  
  
  
    My Photos
    Check out my new photos!
    
    

흔이 우리가 많이 봐왔던 UI인데, 상단에 2개의 TextBlock이 있고 바로 아래에 있는 ListBox에 jpg 이미지에 대한 경로가 나타나 있다.

스타일과 템플릿을 적용해보기에 앞서, 한가지 궁금증을 풀어보도록 하자.

ListBox에 나타난 jpg 이미지 파일의 경로 문자열은 어디서 왔는가? <ListBox>의 속성 중에 ItemsSource가 그 해답으로 가는 길의 시작점이다. ItemSource 속성에 MyPhotos라는 StaticResource를 바인딩하고 있다. 그렇다면 MyPhotos는 무엇인가? <Window.Resources>에 보면 <ObjectDataProvider>를 이용해서 PhotoList라는 클래스를 MyPhotos라는 Key로 생성하고 있다.

이제 PhotoList 클래스에 대해서 살펴보도록 하자. PhotoList는 Photo라는 클래스를 관리해주는 Collection으로써 다음과 같이 정의되어 있다.

public class PhotoList : ObservableCollection
{
    public PhotoList() { }

    public PhotoList(string path) : this(new DirectoryInfo(path)) { }

    public PhotoList(DirectoryInfo directory)
    {
        _directory = directory;
        Update();
    }

    public string Path
    {
        set {
            _directory = new DirectoryInfo(value);
            Update();
        }

        get { return _directory.FullName; }
    }

    public DirectoryInfo Directory
    {
        set
        {
            _directory = value;
            Update();
        }

        get { return _directory; }
    }

    private void Update()
    {
        foreach (FileInfo f in _directory.GetFiles("*.jpg"))
        {
            Add(new Photo(f.FullName));
        }
    }

    DirectoryInfo _directory;
}

먼저, PhotoList는 ObservableCollection<Photo>에서 파생되었는데, ObservableCollection은 콜렉션에서 자신이 관리하고 있는 데이터(여기서는 Photo 클래스의 인스턴스)가 삭제, 추가등과 같은 변경이 있을 경우 통지를 해주는 클래스이며, ListBox 컨트롤의 ItemSource가 될 수 있는 클래스이다. 기본적으로 PhotoList가 하는 일은 특정 폴더경로를 받아서 그 경로에 있는 확장자가 jpg인 파일명을 통해 Photo라는 클래스의 인스턴스를 만들어 준다. 이제 Photo 클래스에 대해서 살펴보자.

public class Photo
{
    public Photo(string path)
    {
        _source = path;
    }

    public override string ToString()
    {
        return Source;
    }

    private string _source;

    public string Source { get { return _source; } }
}

Photo는 무척 간단한 클래스인데, 하는 일은 단지 문자열(여기서는 jpg 파일명)을 속성으로 가지고 있고 ToString을 통해 반환하는 일을 한다.

이제 다시 XAML에서 살펴본 PhotoList 클래스 타입으로써 생성된 MyPhotos라는 Key를 가진 객체에 PhotoList 클래스가 jpg 파일명을 수집할 경로명을 지정해야 하는데, 그것은 해당 XAML에 대한 Code-Behind 코드 안의 WindowLoaded 이벤트에서 이루어지게 된다.

private void WindowLoaded(object sender, RoutedEventArgs e)
{
    Photos = (PhotoList)(this.Resources["MyPhotos"] 
        as ObjectDataProvider).Data;

    Photos.Path = "...\\...\\Images";
}

이제 지금까지의 코드를 기본으로 다양한 스타일과 템플릿을 적용할 준비가 끝났다.

정의되지 않은 클래스 맴버 함수 호출하기

당연이 인스턴스로 생성된 클래스의 맴버함수를 호출하기 위해서는 해당 클래스를 정의하고 있는 헤더 파일을 포함해야 하지만, 헤더 파일을 포함하지 않고 클래스의 맴버함수를 호출해야할 경우가 있다. 즉 정의되지 않는 클래스 인스턴스의 맴버 함수를 호출해야한다.

어떻게 할 수 있을까? 해답은 클래스 맴버 함수 포인터를 사용하는 방법이다.

먼저 사용하는 코드부분에서 호출해야하는 클래스를 선언하고 그 클래스에서 사용해야할 맴버 함수의 포인터를 선언한다.

C사용하는클래스의 헤더파일에…

class C사용될클래스;
typedef void (C사용될클래스::*맴버함수)(void);

그리고 C사용하는클래스의 맴버변수로써 C사용될클래스의 인스턴스와 맴버함수에 대한 변수를 추가한다.

private:
    맴버함수 funcPtr;
    C사용될클래스* p사용될클래스인스턴스;

public:
    void Set(맴버함수 funcPtr, C사용될클래스* pInstance) {
        this.funcPtr = funcPtr;
        p사용될클래스인스턴스 = pInstance;
}

이제 실제로 C사용될클래스의 멤버함수를 사용하는 방법은 아래와 같다.

((*p사용될클래스인스턴스).*funcPtr)();

여기서 주목할 것은 앞에서도 언급했지만 어디서도 C사용될클래스에 대한 헤더파일을 포함하지 않았다는 점이다. 즉, C사용하는클래스는 단지 C사용할클래스를 모르며, 단지 C사용할클래스에서 꼭 필요한 것만 알고 있다는 것이다.

꼼꼼한 독자라면 직감했겠지만, C사용하는클래스의 funcPtr과 p사용될클래스인스턴스는 어떤식으로 값을 설정해야하는가라는 문제가 생긴다.

이것은 C사용될클래스에서 C사용할클래스의 Set함수를 호출해주면 된다. 즉 C사용될클래스의 정의부 어디에선가 다음과 같은 형식의 코드(한가지 예일뿐..)를 호출한다.

p사용하는클래스 = new C사용하는클래스();
p사용하는클래스->Set(&C사용될클래스::Function, this);

사실, 이런 기술(정확히, 솔직이 말한다면 편법)이 필요한 이유는 두개의 클래스가 서로 상호참조를 하는 경우이다. 이런 경우는 서로의 헤더파일을 포함해서 쉽게 구현할 수 있는 경우가 대부분이지만, 만약…. 이 두개의 클래스가 서로 완전이 다른 개념의 개발 프로젝트인 경우에는 예외인데, 예를 들어서 하나의 클래스는 ATL 프로젝트에 있고, 다른 하나는 일반적인 Generic C/C++ 프로젝트인 경우, 단순이 헤더파일을 포함할 경우 서로의 개발환경을 이해하지 못하므로 엄청나게 많은 에러 메세지를 쏟아 낼 것이고, 머리속이 하얗게  되는 것을 경험할 것이다. 이 편법을 사용하지 않는 것이 제대로 개발하고 있다는 증거인지라, 사용하지 않기를 바라지만 꼭 필요한 경우라면 요긴할 것으로 판단되어 글로 정리하여 남긴다.