C++ 블로그 뷰어 앱 만들기(C++)

2014.05.30 15:27

졸리운_곰 조회 수:3956



[출처] http://msdn.microsoft.com/ko-kr/library/windows/apps/hh465045.aspx

블로그 뷰어 앱 만들기(C++)


전체 프로세스: 다음은 C++ 및 XAML(Extensible Application Markup Language)을 사용하여 RSS 2.0 또는 Atom 1.0 피드에서 블로그를 찾고 읽는 데 사용할 수 있는 Windows 스토어 앱을 개발하는 방법입니다.

이 자습서에서는 독자가 C++를 사용하여 첫 Windows 스토어 앱 만들기에 나오는 단원 1-4의 개념에 이미 익숙하다고 가정합니다.

이 앱의 완료된 버전을 연구해 보려면 MSDN 코드 갤러리 웹 사이트에서 다운로드할 수 있습니다.

이 자습서를 살펴본 후 C++ 및 XAML을 사용하여 종단 간 Windows 스토어 앱 개발: Hilo를 읽어 보면 C++로 개발한 Windows 스토어 앱에서 최신 C++, Windows 런타임, 비동기 프로그래밍, XAML, 개발 패턴(예: MVVM(Model-View-ViewModel)을 사용하는 방법에 대해 자세히 알 수 있습니다.

다른 프로그래밍 언어를 사용하려면 다음 항목을 참조하세요.

목표

이 자습서는 여러 페이지로 된 Windows 스토어 앱을 만드는 방법과 Windows 런타임에 대한 코딩 작업을 간소화하기 위해 Visual C++ 구성 요소 확장(C++/CX)을 사용하는 방법 및 시기에 대해 지식을 제공하기 위한 것입니다. 또한 concurrency::task 클래스를 사용하여 비동기 Windows 런타임 API를 이용하는 방법도 안내합니다.

SimpleBlogReader 앱의 기능은 다음과 같습니다.

  • 여러 블로그 피드를 지원합니다.
  • PLM(프로세스 수명 관리)을 지원하며, 포그라운드의 다른 작업으로 인해 시스템에서 앱을 종료하는 경우 해당 상태를 정확히 저장하고 다시 로드합니다.
  • 서로 다른 창 크기 및 장치 방향(가로 또는 세로)에 맞춰 조정됩니다.
  • 간단한 애니메이션과 전환을 사용해 사용자 인터페이스에 생기를 불어넣습니다.
  • 앱의 모양을 시각적으로 개선하기 위해 스타일을 사용합니다.

1부: 프로젝트 만들기

먼저, C++ Windows 스토어 앱의 빈 앱 템플릿을 사용하여 프로젝트를 만들겠습니다.

Hh465045.wedge(ko-kr,WIN.10).gif프로젝트를 만들려면

  1. Visual Studio에서 파일 > 새로 만들기 > 프로젝트를 선택한 다음 설치됨 > Visual C++ > Windows 스토어빈 앱(XAML) 템플릿을 차례로 선택합니다. 더 자세한 지침은 C++으로 작성된 Hello World를 참조하세요.

  2. 프로젝트의 이름을 "SimpleBlogReader"로 지정합니다.

앱 기능 지정

Windows 스토어 앱은 파일 시스템, 네트워크 리소스, 하드웨어에 대한 액세스가 제한된 보안 컨테이너에서 실행됩니다. 사용자가 Windows 스토어에서 앱을 설치하면 Windows는 Package.appxmanifest 파일에서 해당 메타데이터를 확인하여 앱에 필요한 기능을 파악합니다. 예를 들어 앱이 인터넷의 데이터, 사용자 문서 라이브러리의 문서 또는 사용자 웹캠 및 마이크에 액세스해야 할 수 있습니다. 앱이 설치되면 필요한 기능을 사용자에게 표시하며 앱이 해당 리소스에 액세스하려면 먼저 사용자가 사용 권한을 부여해야 합니다. 앱이 필요한 리소스에 대한 사용 권한을 받지 못하면 런타임에서 해당 리소스에 대한 액세스가 허용되지 않습니다. 블로그 뷰어를 사용하려면 인터넷 액세스가 필요하므로, 인터넷이 요청되었는지 확인하겠습니다.

Hh465045.wedge(ko-kr,WIN.10).gif기본 인터넷 기능이 요청되었는지 확인하려면

  1. 솔루션 탐색기에서 Package.appxmanifest를 엽니다. 응용 프로그램 매니페스트 디자이너에서 파일이 열립니다.

  2. 기능 탭을 선택합니다.

    빈 앱 템플릿의 설정을 기반으로 인터넷(클라이언트) 확인란이 이미 선택되어 있습니다. 다른 기능이 필요한 경우 이 디자이너에서 기능을 선택할 수 있습니다.

  3. 매니페스트 디자이너를 닫습니다.

일반적으로 응용 프로그램 매니페스트 디자이너를 사용하여 기능을 지정하고 나면 디자이너에서 Package.appxmanifest.xml 파일의 Capabilities 요소에 사양을 작성하므로, 수동으로 수정할 필요가 없습니다. 하지만 여기서는 XML 텍스트 편집기에서 Package.appxmanifest.xml을 열어 Capabilities 요소가 어떻게 구성되어 있는지 살펴보겠습니다.

<Capabilities>    
    <Capability Name="internetClient" />
</Capabilities>


앱 기능에 대한 자세한 내용은 매니페스트 디자이너를 참조하세요.

2부: 앱으로 데이터 가져오기

블로그 피드를 앱으로 가져오는 코드를 작성할 수 있습니다. "Windows용 개발" 블로그에는 해당 게시물 전체 텍스트가 RSS 및 Atom 형식 둘 다로 공개되어 있습니다. 앱에서 각 최신 블로그 게시물의 제목, 만든 이, 날짜, 콘텐츠를 표시하고자 합니다. Windows::Web::Syndication 네임스페이스의 클래스를 사용하여 피드를 다운로드할 수 있습니다. (이 클래스를 사용하여 UI에 데이터를 표시할 수도 있지만, 그 대신 RSS 및 Atom 피드를 같은 방식으로 처리할 수 있도록 고유한 데이터 클래스를 만들겠습니다.) 다음 세 가지 클래스를 만들어 봅니다.

  • FeedData는 RSS 또는 Atom 피드에 대한 정보를 포함합니다.

  • FeedItem은 피드의 개별 블로그 게시물에 대한 정보를 포함합니다.

  • FeedDataSource에는 피드를 다운로드하고 데이터 클래스를 초기화할 메서드가 포함되어 있습니다.

이러한 요소를 public ref 클래스로 정의하여 제목, 만든 이 등을 표시하는 XAML 요소에 데이터를 바인딩할 수 있도록 합니다. Bindable 특성을 사용하여 이러한 유형의 인스턴스에 동적으로 바인딩하고 있음을 XAML 컴파일러에 나타냅니다. public ref 클래스에서 public 데이터 멤버는 속성으로 표시됩니다. 특별한 논리가 없는 속성에는 사용자가 지정한 getter 및 setter가 필요하지 않으며 컴파일러에서 제공합니다. FeedData 클래스에서는 IVector를 사용하여 public 컬렉션 유형을 다른 Windows 런타임 클래스 및 구성 요소에 표시합니다. 또한 IVector를 구현하는 구체적 형식으로 내부적으로 Platform::Collections::Vector 클래스를 사용합니다. 뒷부분에서 해당 형식을 사용하는 방법을 살펴보겠습니다.

Hh465045.wedge(ko-kr,WIN.10).gif사용자 지정 데이터 클래스를 만들려면

  1. 솔루션 탐색기SimpleBlogReader 프로젝트 노드에 대한 바로 가기 메뉴에서 추가 > 새 항목을 선택합니다.

  2. 헤더 파일(.h) 옵션을 선택하고 FeedData.h로 이름을 지정합니다.

  3. FeedData.h를 열고 다음 코드를 복사하여 여기에 붙여 넣습니다. 잠시 코드를 살펴보고 C++/CX 구문을 익혀 두세요. "pch.h"에 대한 #include 지시문을 확인하세요. <string>, <vector>, 기타 시스템 헤더를 넣을 위치입니다. 기본적으로 pch.h는 Platform::Collections::Vector 형식에 필요한 collection.h를 포함합니다. 자세한 내용은 클래스 및 구조(C++/CX)를 참조하세요.

    //feeddata.h
    
    #pragma once
    #include "pch.h"
    
    
    namespace SimpleBlogReader
    {
        // To be bindable, a class must be defined within a namespace
        // and a bindable attribute needs to be applied.
        // A FeedItem represents a single blog post.
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedItem sealed
        {
        public:
            FeedItem(void){}
    
            property Platform::String^ Title;
            property Platform::String^ Author;
            property Platform::String^ Content;      
            property Windows::Foundation::DateTime PubDate;      
            property Windows::Foundation::Uri^ Link;
    
        private:
            ~FeedItem(void){}
        };
    
        // A FeedData object represents a feed that contains 
        // one or more FeedItems. 
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedData sealed
        {
        public:
            FeedData(void)
            {
                m_items = ref new Platform::Collections::Vector<FeedItem^>();
            }
    
            // The public members must be Windows Runtime types so that
            // the XAML controls can bind to them from a separate .winmd.
            property Platform::String^ Title;            
            property Windows::Foundation::Collections::IVector<FeedItem^>^ Items
            {
                Windows::Foundation::Collections::IVector<FeedItem^>^ get() {return m_items; }
            }
    
            property Platform::String^ Description;
            property Windows::Foundation::DateTime PubDate;
            property Platform::String^ Uri;
    
        private:
            ~FeedData(void){}
    
            Platform::Collections::Vector<FeedItem^>^ m_items;
        };   
    
        // A FeedDataSource represents a collection of FeedData objects
        // and provides the methods to download the source data from which
        // FeedData and FeedItem objects are constructed. This class is 
        // instantiated at startup by this declaration in the 
        // ResourceDictionary in app.xaml: <local:FeedDataSource x:Key="feedDataSource" />
        [Windows::UI::Xaml::Data::Bindable]
        public ref class FeedDataSource sealed
        {
        private:
            Platform::Collections::Vector<FeedData^>^ m_feeds;
            std::map<Platform::String^, concurrency::task_completion_event<FeedData^>> m_feedCompletionEvents;
            FeedData^ GetFeedData(Platform::String^ feedUri, Windows::Web::Syndication::SyndicationFeed^ feed);       
    
        public:
            FeedDataSource();
            property Windows::Foundation::Collections::IObservableVector<FeedData^>^ Feeds
            {
                Windows::Foundation::Collections::IObservableVector<FeedData^>^ get()
                {
                    return this->m_feeds;
                }
            }
            void InitDataSource();
            static Windows::Foundation::IAsyncOperation<FeedData^>^ GetFeedAsync(Platform::String^ title);
            static FeedItem^ GetFeedItem(FeedData^ fd, Platform::String^ uniqueiD);
        };
    }
    
    
  4. 이제 새 .cpp 파일을 만듭니다. SimpleBlogReader 프로젝트 노드의 바로 가기 메뉴에서 추가 > 새 항목을 선택한 다음 C++ 파일(.cpp) 옵션을 선택하고 파일 이름을 FeedData.cpp로 지정합니다. 파일을 열고 다음 코드를 복사하여 해당 파일에 붙여 넣습니다.

    #include "pch.h"
    #include "FeedData.h"
    
    
    using namespace std;
    using namespace concurrency;
    using namespace SimpleBlogReader;
    using namespace Platform;
    using namespace Platform::Collections;
    using namespace Windows::Foundation;
    using namespace Windows::Web::Syndication;
    
    
    
    FeedDataSource::FeedDataSource()
    {
        m_feeds = ref new Vector<FeedData^>();
    }
    
    // Retrieve the data for each atom or rss feed and put it
    // into our custom data structures.
    void FeedDataSource::InitDataSource()
    {
        // Left as an exercise: store the urls separately and let the user configure them.
        // It might be more convenient to use Platform::Strings here, but using wstring 
        // serves to demonstrate how standard C++ types can be used here.
        std::vector<std::wstring> urls; 
        urls.push_back(L"http://windowsteamblog.com/windows/b/developers/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");
    
        urls.push_back(L"http://windowsteamblog.com/windows/b/business/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/springboard/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");
        // There is no Atom feed for this blog, so we use the RSS feed.
        urls.push_back(L"http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows_live/b/developer/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/ie/b/ie/atom.aspx");
        urls.push_back(L"http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");
    
        // If we are resuming, we need to create a map of completion events so that
        // we don't attempt to restore page's state before it's backing data has been loaded.
        // First we create all the  events in an "unset" state, mapped to the urls as keys.
        // we'll set the event after we asynchronously load the feed.
        for( wstring url : urls)
        {
            auto uri = ref new String(url.c_str());
            task_completion_event<FeedData^> e;
            m_feedCompletionEvents.insert(make_pair(uri, e));
        }
    
        SyndicationClient^ client = ref new SyndicationClient();   
    
        // Range-based for loop. Never write a regular for loop again!
        for(wstring url : urls)
        {
            // Create the async operation. feedOp is an 
            // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
    
            auto uri = ref new String(url.c_str());
            auto feedUri = ref new Uri(uri);
            auto feedOp = client->RetrieveFeedAsync(feedUri);
    
            // Create the task object and pass it the async operation.
            // SyndicationFeed^ is the type of the return value
            // that the feedOp operation will eventually produce.       
    
            // Then, initialize a FeedData object with the feed info. Each
            // operation is independent and does not have to happen on the
            // UI thread. Therefore, we specify use_arbitrary.
            create_task(feedOp)
    
            .then([this, uri]  (SyndicationFeed^ feed) -> FeedData^
            {
                return GetFeedData(uri, feed);
            }, concurrency::task_continuation_context::use_arbitrary())
    
    
            // Append the initialized FeedData object to the list
            // that is the data source for the items collection.
            // This has to happen on the UI thread. By default, a .then
            // continuation runs in the same apartment thread that it was called on.
            // Because the actions will be synchronized for us, we can append 
            // safely to the Vector without taking an explicit lock.
            .then([this] (FeedData^ fd)
            {
                m_feeds->Append(fd);
                m_feedCompletionEvents[fd->Uri].set(fd);
    
                // Write to VS output window in debug mode only. Requires <windows.h>.
                OutputDebugString(fd->Title->Data());
                OutputDebugString(L"\r\n");
            })
    
            // The last continuation serves as an error handler. The
            // call to get() will surface any exceptions that were raised
            // at any point in the task chain.
            .then( [] (task<void> t)
            {
                try
                {
                    t.get();
                }
                // SyndicationClient throws Platform::InvalidArgumentException 
                // if a URL contains illegal characters.
                // We catch this exception for demonstration purposes only.
                // In the current design of this app, an illegal
                // character can only be introduced by a coding error
                // and should not be caught. If we modify the app to allow
                // the user to manually add a new url, then we need to catch
                // the exception.
                catch(Platform::InvalidArgumentException^ e)
                {
                    // For example purposes we just output error to console.
                    // In a real world app that allowed the user to enter
                    // a url manually, you could prompt them to try again.
                    OutputDebugString(e->Message->Data());
                }
            }); //end task chain
        };
    }
    
    
    FeedData^ FeedDataSource::GetFeedData(String^ feedUri, SyndicationFeed^ feed)
    {
    
        FeedData^ feedData = ref new FeedData();
    
        // Knowing this makes it easier to map completion_events 
        // when we resume from termination.
        feedData->Uri = feedUri;
    
        // Get the title of the feed (not the individual posts).
        feedData->Title = feed->Title->Text; 
    
        if (feed->Subtitle->Text != nullptr)
        {
            feedData->Description = feed->Subtitle->Text;
        }	 
        // Use the date of the latest post as the last updated date.
        feedData->PubDate = feed->Items->GetAt(0)->PublishedDate;	
    
        // Construct a FeedItem object for each post in the feed
        // using a range-based for loop. Preferable to a 
        // C-style for loop, or std::for_each.
        for (auto  item : feed->Items)
        {
            auto feedItem = ref new FeedItem();
            feedItem->Title = item->Title->Text; 
            feedItem->PubDate = item->PublishedDate;		
    
            //We only get first author in case of multiple entries.
            feedItem->Author = item->Authors->GetAt(0)->Name; 
    
            if (feed->SourceFormat == SyndicationFormat::Atom10)
            {
                feedItem->Content = item->Content->Text;
                feedItem->Link = ref new Uri(item->Id);
            }
    
            else if (feed->SourceFormat == SyndicationFormat::Rss20)
            {
                feedItem->Content = item->Summary->Text;
                feedItem->Link = item->Links->GetAt(0)->Uri;
            }
    
            feedData->Items->Append(feedItem);
        };
    
        return feedData;
    
    } //end GetFeedData
    
    
    // We use this method to get the proper FeedData object when resuming
    // from shutdown. We need to wait for this data to be populated before
    // we attempt to restore page state. Note the use of task_completion_event
    // which doesn't block the UI thread.
    IAsyncOperation<FeedData^>^ FeedDataSource::GetFeedAsync(String^ uri)
    {
        return create_async([uri]()
        {
            auto feedDataSource = safe_cast<FeedDataSource^>( 
                App::Current->Resources->Lookup("feedDataSource"));
    
            // Does not block the UI thread.
            auto f = feedDataSource->m_feedCompletionEvents[uri];
    
            // In the callers we continue from this task after the event is 
            // set in InitDataSource and we know we have a FeedData^.
            task<FeedData^> t = create_task(f);
            return t;
        });
    }
    
    // We stored the stringID when the app was suspended
    // because storing the FeedItem itself would have required
    // more custom serialization code. Here is where we retrieve
    // the FeedItem based on its string ID.
    FeedItem^ FeedDataSource::GetFeedItem(FeedData^ feed, String^ uniqueId)
    {
        auto itEnd = end(feed->Items);
        auto it = std::find_if(begin(feed->Items), itEnd, 
            [uniqueId] (FeedItem^ fi)
        {
            return fi->Title == uniqueId;
        });
    
        if (it != itEnd)
            return safe_cast<FeedItem^>(*it);
    
        return nullptr;
    }
    
    
    

    Windows.Web.Syndication.SyndicationClient 클래스는 RSS 및 Atom 피드를 검색하고 구문 분석합니다. 이 작업에는 네트워크 I/O가 필요하므로 메서드가 비동기적으로 실행됩니다. 비동기 프로그래밍 모델은 Windows 런타임 클래스 라이브러리 전체에서 발견됩니다. 비동기 메서드 호출은 백그라운드 스레드에서 작업이 수행되는 동안 UI가 응답 가능 상태로 유지되도록 제어를 즉시 UI 스레드로 반환합니다.

    Windows 런타임에서는 비동기 작업을 호출하고 작업이 완료되면 결과를 가져오는 방법을 제공합니다. 따라서 해당 API에 대해 직접 프로그래밍을 수행할 수 있습니다. 그러나 pch.h에 포함된 ppltasks.h에 정의된 task class를 사용하는 것이 좋습니다. task 클래스는 동일한 비동기 Windows 런타임 API를 호출하지만 해당 API를 사용하여 더 간결한 코드를 작성할 수 있으며, 보다 쉽게 비동기 작업을 체인으로 연결하고 발생하는 모든 예외를 한 지점에서 처리할 수 있습니다. 방금 만든 파일의 두 위치에서 task 클래스를 사용합니다. task 클래스를 사용하는 경우 기본 단계는 항상 같습니다.

    1. Windows 런타임*Async 메서드(예: Windows::Web::Syndication::ISyndicationClient::RetrieveFeedAsync)를 호출하여 비동기 작업을 만듭니다.

    2. concurrency::create_task를 호출하고 이 작업을 입력 매개 변수로 사용하여 task 개체를 만듭니다.

    3. task::then을 호출하고 작업의 반환 값을 입력으로 사용하는 람다를 지정하여 원래 작업이 완료되면 실행될 작업을 정의합니다.

    4. 선택적으로, then을 한 번 이상 다시 호출합니다. 이러한 절은 이전 절의 반환 값을 사용할 수 있습니다.

    5. 작업 체인에서 발생한 모든 예외를 처리하는 최종 then 절을 제공합니다. 이 단계는 옵션이지만 수행하는 것이 좋습니다.

3부: 사용자 지정 날짜 형식 클래스 만들기

이제 사용자 지정 클래스 즉, 임의 방식으로 임의 형식 간에 변환할 수 있게 해 주는 인터페이스인 IValueConverter의 구현을 하나 더 추가할 수 있습니다. 여기에서는 DateTime 인수가 제공될 때 일, 월, 연도를 개별 문자열로 반환하는 Date Converter를 추가합니다. 이 클래스는 나중에 그리드 항목 및 목록 보기 항목에 대해 정의할 비주얼 스타일에서 사용합니다.

Hh465045.wedge(ko-kr,WIN.10).gifIValueConverter를 구현하는 클래스를 만들려면

  • SimpleBlogReader 프로젝트에서 이름이 DateConverter.h인 새 헤더를 추가한 다음 여기에 다음 구현을 붙여 넣습니다.

    //DateConverter.h
    
    #pragma once
    #include <string> //for wcscmp
    
    namespace SimpleBlogReader
    {
    
    public ref class DateConverter sealed : public Windows::UI::Xaml::Data::IValueConverter  
    {
        public:
        virtual Platform::Object^ Convert(Platform::Object^ value,
                                          Windows::UI::Xaml::Interop::TypeName targetType,
                                          Platform::Object^ parameter,
                                          Platform::String^ language)
        {		
            if(value == nullptr)
            {
                throw ref new Platform::InvalidArgumentException();
            }
            auto dt = safe_cast<Windows::Foundation::DateTime>(value);
            auto param = safe_cast<Platform::String^>(parameter);
            Platform::String^ result;
            if(param == nullptr)
            {
                auto dtf =
                Windows::Globalization::DateTimeFormatting::DateTimeFormatter::ShortDate::get();
                result = dtf->Format(dt);
            }
            else if(wcscmp(param->Data(), L"month") == 0)
            {
                auto month = 
                    ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{month.abbreviated(3)}");
                result = month->Format(dt);
            }
            else if(wcscmp(param->Data(), L"day") == 0)
            {
                auto month = 
                   ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{day.integer(2)}");
                result = month->Format(dt);
            }
            else if(wcscmp(param->Data(), L"year") == 0)
            {
                auto month = 
                    ref new Windows::Globalization::DateTimeFormatting::DateTimeFormatter("{year.full}");
    				        result = month->Format(dt);
            }
            else
            {
                // We don't handle other format types currently.
                throw ref new Platform::InvalidArgumentException();
            }
    
            return result; 
         }
    
        virtual Platform::Object^ ConvertBack(Platform::Object^ value,
                                              Windows::UI::Xaml::Interop::TypeName targetType,
                                              Platform::Object^ parameter,
                                              Platform::String^ language)
        {   
            // Not needed in SimpleBlogReader. Left as an exercise.
            throw ref new Platform::NotImplementedException();
        }
    };
    }
    
    

Hh465045.wedge(ko-kr,WIN.10).gif앱이 사용자 지정 클래스를 인식하도록 만들려면

  1. 앱, 특히 해당 XAML 컨트롤이 사용자 지정클래스를 인식하도록 만들려면 먼저 헤더 파일을 포함하여 컴파일되도록 해야 합니다. 다음 코드를 App.xaml.h에 추가하면 됩니다.
    #include "FeedData.h"
    #include "DateConverter.h"
    
    
  2. FeedDataSource와 DateConverter는 앱이 활성화되어 있는 동안 활성 상태로 유지하려고 하므로 앱 리소스로 저장합니다. 그러면 앱이 시작될 때 인스턴스화됩니다. 다음 코드에 정의된 x:Key 값을 사용하여 XAML 및 코드 숨김으로 된 앱 전체에서 이 인스턴스를 참조하겠습니다.

    이 참조를 구현하려면 App.xaml에서 응용 프로그램 요소의 닫는 태그 바로 앞에 <Application.Resources>를 입력하거나 동적 문 완성을 사용하여 이 요소를 추가합니다. 그런 다음, <Application> 노드가 다음과 같은 모양이 되도록 FeedDataSource 및 DateConverter 리소스를 추가합니다.

     <Application
        x:Class="SimpleBlogReader.App"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:SimpleBlogReader">
        <Application.Resources>
            <local:FeedDataSource x:Key="feedDataSource" />
            <local:DateConverter x:Key="dateConverter" />
        </Application.Resources>
    </Application>
    
    
    
  3. 이제, 앱이 현재는 아무 동작을 하지 않더라도 오류 없이 컴파일되는지 확인해야 합니다. 앱의 디버깅 인스턴스를 컴파일하고 표시하려면 F5 키를 누릅니다. 개발 작업으로 돌아가려면 Visual Studio로 되돌아가서 Shift+F5를 누릅니다.

4부: 페이지 및 탐색 추가

이제 UI를 만들 준비가 되었습니다. 여러 블로그를 지원하려면 앱에 페이지를 추가하고 페이지 간의 탐색을 처리해야 합니다. 다행히 Visual Studio에서는 필요한 대부분의 탐색 및 방향 기능을 자동으로 구현하는 다양한 페이지 템플릿을 제공합니다.

먼저 Windows 팀 블로그를 나열하는 페이지가 필요합니다. 이를 위해 항목 페이지 템플릿을 사용할 수 있습니다. 독자가 이 페이지에서 블로그를 선택하면 해당 블로그의 게시물 목록을 분할 페이지 템플릿 기반의 다른 페이지로 로드합니다. 또한 기본 페이지 템플릿을 사용하여 목록 보기를 표시하지 않고 개별 블로그 게시물을 읽을 수 있도록 세부 정보 페이지를 추가하겠습니다. 이 템플릿은 필요한 탐색을 자동 지원하므로, 개발자는 직접 코딩할 필요가 없이 각 코드 숨김 클래스에서 스텁되는 LoadStateSaveState 메서드에 대한 사용자 지정 논리를 작성하고, 분할 페이지에서 세부 정보 페이지로 이동할 수 있도록 단추가 있는 앱 바를 추가하는 등의 주 작업에 집중할 수 있습니다.

Hh465045.wedge(ko-kr,WIN.10).gif앱에 페이지를 추가하려면

  1. 솔루션 탐색기에서 MainPage.xaml에 대한 바로 가기 메뉴를 연 다음 제거를 선택합니다. 이 프로젝트에서는 이 페이지 또는 해당하는 코드 숨김 파일을 사용하지 않습니다. 영구적으로 삭제하거나 단순히 제거할 수 있습니다.

  2. App.xaml.cpp에서 MainPage.xaml.h에 대한 #include 지시문을 #include ItemsPage.xaml.h로 변경합니다.

  3. 계속 App.xaml.cpp에서 Ctrl+H를 눌러 "MainPage" 인스턴스를 찾고 이를 "ItemsPage"로 바꿉니다. "ItemsPage" 인스턴스에 빨간 물결선 밑줄이 나타나며 새 페이지를 추가하면 이 밑줄이 사라집니다.

  4. SimpleBlogReader 프로젝트에 대한 바로 가기 메뉴를 열고 추가 > 새 항목을 선택합니다.

  5. 새 항목 추가 대화 상자의 설치됨 창에서 Visual C++ > Windows 스토어를 선택합니다.

    가운데 창에서 항목 페이지를 선택하고 기본 이름을 그대로 사용하도록 둔 채로 추가 단추를 선택합니다.

    필요한 파일을 자동으로 추가할지 여부를 묻는 대화 상자에서 를 선택합니다. 이 파일에는 탐색뿐 아니라 앱 종료와 다시 시작 시 단순 형식의 직렬화 및 역직렬화를 지원하는 코드가 들어 있습니다. 이 파일은 \Common\ 폴더에 추가됩니다.

  6. 이전 단계를 반복하고 분할 페이지 템플릿을 선택합니다.

  7. 단계를 다시 반복하고 기본 페이지 템플릿을 선택한 후 페이지 이름을 DetailPage로 지정합니다.

항목 페이지에는 Windows 팀 블로그 목록이 표시됩니다. 분할 페이지의 왼쪽에는 각 블로그의 게시물이 표시되고 오른쪽에는 선택한 게시물의 콘텐츠가 표시됩니다. 세부 정보 페이지에는 선택한 게시물 내용, 뒤로 단추, 페이지 제목만 표시됩니다. 이 페이지에서는 분할 페이지에서처럼 HTML 문자열에서 WebView로 게시물 콘텐츠를 로드하는 대신 게시물의 URL을 탐색하여 실제 웹 페이지를 표시합니다. 이 구현을 마치면 앱의 페이지가 다음과 같이 표시됩니다.

b01.IC618536.png


추가한 페이지의 XAML 및 코드 숨김을 살펴보면 이러한 페이지 템플릿이 많은 작업을 수행하는 것을 확실히 알 수 있습니다. 처음에는 놓치기 쉽기 때문에, 각 페이지 템플릿이 세 개의 주요 섹션으로 구성되어 있다는 사실을 알고 있으면 많은 도움이 됩니다.

리소스

리소스 섹션에는 페이지의 스타일 및 데이터 템플릿이 정의되어 있습니다. 스타일을 사용하여 일관된 모양 만들기 섹션에서 좀 더 자세히 다룹니다.

Visual State Manager

VSM(Visual State Manager)에는 여러 레이아웃과 방향에 맞게 조정되는 애니메이션 및 전환이 정의되어 있습니다. 다양한 레이아웃에 맞게 조정 섹션에서 좀 더 자세히 다룹니다.

앱 콘텐츠

앱 UI를 구성하는 컨트롤 및 콘텐츠는 루트 레이아웃 패널에 정의되어 있습니다.

 

페이지 간 이동

Windows 스토어 앱의 주요 활동 중 하나는 페이지 간 이동입니다. 사용자는 페이지 간에 이동하기 위해 앞으로 또는 뒤로 단추를 선택하거나, 항목에 대한 세부 정보를 표시하는 페이지와 같은 다른 페이지를 여는 항목을 선택할 수 있습니다. SimpleBlogReader를 위한 탐색 디자인은 다음과 같습니다.

  • 앱이 시작되면 항목 페이지에 블로그 피드(DataFeed 개체)의 그리드가 표시됩니다. 설명(있는 경우) 및 최신 게시물 날짜와 함께 블로그 제목만 표시됩니다. 사용자가 피드를 선택하면 앱이 분할 페이지로 이동합니다.

  • 앱이 가로 방향으로 표시되는 경우 분할 페이지는 왼쪽에 게시물(FeedItem 개체)의 목록을, 오른쪽에 현재 선택된 게시물의 미리 보기를 표시합니다. 세로 방향으로 표시되는 경우 분할 페이지는 목록 또는 미리 보기를 표시하지만, 둘을 동시에 표시하지는 않습니다. 사용자가 목록에서 다른 항목을 선택하면 미리 보기가 바뀝니다.

  • 분할 페이지에는 사용자가 화면의 위쪽부터 살짝 밀거나 마우스 오른쪽 단추를 클릭하여 호출할 수 있는 앱 바가 있습니다. 사용자가 앱 바의 단추를 선택하면 앱은 블로그 게시물을 완전한 웹 페이지로 표시하는 세부 정보 페이지로 이동합니다.

  • 세부 정보 페이지에서 사용자는 뒤로 단추를 선택하여 분할 페이지로 돌아갈 수 있습니다. 분할 페이지에도 항목 페이지로 돌아가는 뒤로 단추가 있습니다.

상황이 다소 복잡하지만, 페이지를 로드할 때마다 탐색이 사용자 동작(앞으로 또는 뒤로)에 의해 호출되는지, 아니면 과거 일정 시점에 중단된 후 시스템에서 앱을 다시 시작하여 호출되는지 고려해야 합니다.

\Common\ 폴더의 SuspensionManager.h 및 SuspensionManager.cpp에 있는 SuspensionManager 클래스는 앱이 일시 중단되거나 종료될 때 나중에 복원될 수 있도록 페이지 상태를 저장하는 데 필요한 코드의 상당 부분을 제공합니다. 그러나 페이지를 로드하거나 다시 로드하기 위해 필요한 특정 논리는 직접 구현해야 합니다.

XAMLUI 프레임워크는 FramesPages를 사용하고 웹 브라우저에서 탐색과 거의 비슷한 작업을 하는 기본 제공 탐색 모델을 제공합니다. Frame 컨트롤은 Pages를 호스트하며, 방문한 페이지 간에 이동하는 데 사용할 수 있는 탐색 기록을 유지합니다. 또한, 탐색하는 페이지 간에 데이터를 전달할 수 있습니다.

앱이 시작될 때 탐색 기록이 시작됩니다. 탐색을 지원하는 인프라는 App 클래스에 있습니다. Visual Studio 프로젝트 템플릿에서 rootFrame이라는 Frame은 앱 창의 콘텐츠로 설정됩니다. App.xaml.cpp에서 템플릿이 제공하는 코드를 살펴보겠습니다. rootFrame이 설정되면 앱은 현재 상태를 확인하는데, 그 이유는 앱이 종료된 상태에서 시작될 수도 있고 일시 중단 상태에서 다시 시작될 수도 있으며 메모리에 콘텐츠가 이미 있을 수도 있기 때문입니다. 앱이 종료된 상태에서 시작되는 경우 종료될 때 저장된 상태를 로드해야 합니다. 앱은 이런 다양한 상황을 처리한 후 첫 번째 창으로 이동합니다.

// Default implementation. Don't paste this into SimpleBlogReader.
 
/// <summary>
/// Invoked when the application is launched normally by the end user.  Other entry points
/// will be used when the application is launched to open a specific file, to display
/// search results, and so forth.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ e)
{

#if _DEBUG
    if (IsDebuggerPresent())
    {
        DebugSettings->EnableFrameRateCounter = true;
    }
#endif

    auto rootFrame = dynamic_cast<Frame^>(Window::Current->Content);

    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (rootFrame == nullptr)
    {
        // Create a Frame to act as the navigation context and associate it with
        // a SuspensionManager key
        rootFrame = ref new Frame();

        if (e->PreviousExecutionState == ApplicationExecutionState::Terminated)
        {
            // TODO: Restore the saved session state only when appropriate, scheduling the
            // final launch steps after the restore is complete

        }

        if (rootFrame->Content == nullptr)
        {
            // When the navigation stack isn't restored navigate to the first page,
            // configuring the new page by passing required information as a navigation
            // parameter
            if (!rootFrame->Navigate(TypeName(ItemsPage::typeid), e->Arguments))
            {
                throw ref new FailureException("Failed to create initial page");
            }
        }
        // Place the frame in the current Window
        Window::Current->Content = rootFrame;
        // Ensure the current window is active
        Window::Current->Activate();
    }
    else
    {
        if (rootFrame->Content == nullptr)
        {
            // When the navigation stack isn't restored navigate to the first page,
            // configuring the new page by passing required information as a navigation
            // parameter
            if (!rootFrame->Navigate(TypeName(ItemsPage::typeid), e->Arguments))
            {
                throw ref new FailureException("Failed to create initial page");
            }
        }
        // Ensure the current window is active
        Window::Current->Activate();
    }
}


페이지 간 이동을 위해 Frame 컨트롤의 Navigate, GoForward, GoBack 메서드를 사용합니다. Navigate(TypeName, Object) 메서드를 사용하면 새 페이지로 이동하고 동시에 데이터를 전달할 수 있습니다. 첫 번째 매개 변수는 탐색 대상 페이지의 TypeName입니다. 정적 typeid 연산자를 사용하여 유형의 TypeName을 구합니다. 여기에서는 사용자가 앱을 시작하거나 다시 시작할 때 표시될 첫 번째 페이지로서 항목 페이지로 이동하고자 합니다.

모든 페이지에는 비공개 멤버로 NavigationHelper 개체가 있습니다. 이 개체는 자동 생성되며 \Common\ 폴더의 NavigationHelper.h 및 NavigationHelper.cpp에 있습니다. 페이지 탐색 시 NavigationHelper는 탐색 인프라의 기본 관리를 처리합니다. 작업 중 하나는 적절한 시기에 LoadStateSaveState를 호출하는 것입니다. 앱 개발자가 해야 할 일은 페이지의 LoadStateSaveState 메서드에 논리를 추가하여, 페이지에 바인딩할 올바른 데이터가 항상 있도록 하고 앱이 종료될 때 그 데이터를 검색할 수 있도록 하는 것입니다.

LoadStateSaveState의 첫 번째 매개 변수는 탐색 명령을 전송한 페이지입니다. 두 번째 매개 변수는 LoadStateEventArgs입니다. 여기에는 무엇보다도, Navigate 호출 시 전달한 데이터를 포함하는 PageState 속성이 포함되어 있습니다. SuspensionManager는 이 매개 변수를 통해 전달되는 개체를 직렬화합니다. 하지만 개발자의 조작이 없을 때는 형식이 문자열, GUID 또는 기본 형식인 경우에만 성공할 수 있습니다. 따라서 전체 FeedData 개체를 전달하지 않을 것이지만, 문자열을 전달하여 특정 FeedData 또는 FeedItem 개체를 조회하기 위한 키로 사용할 수 있습니다. 그것이 FeedDataSource 클래스에 있는 GetFeedAsyncGetFeedItem 메서드의 용도입니다. 나중에 이러한 메서드에 대해 자세히 살펴보겠습니다.

Hh465045.wedge(ko-kr,WIN.10).gifApp 클래스에서 항목 페이지로 이동하려면

  1. App.xaml.cpp에서 App::OnLaunched 메서드는 rootFrame이 이미 있는지 확인하며, 없는 경우 코드에서 만들어집니다. 새로운 rootFrame을 만들려면 피드 항목도 다운로드하여 초기화해야 합니다. 시스템에 의해 종료되거나 사용자가 닫은 이후 앱을 다시 시작해야 이 코드에 도달하게 됩니다. 이 두 가지 이벤트가 모두 발생하지 않으면 피드 항목은 메모리에 머물러 있습니다.

    App.xaml.cpp의 OnLaunched 메서드에서 rootFrame = ref new Frame(); 문 다음에 이 코드를 추가합니다.

        FeedDataSource^ feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
        if (feedDataSource->Feeds->Size == 0)
        {
            feedDataSource->InitDataSource();
        }
    
    
    

    Navigate 메서드를 App::OnLaunched에서 호출하면 실제로 ItemsPage::LoadState 이벤트 처리기가 호출됩니다. 앞에서 설명했듯이, 직접 직렬화를 수행하지 않을 것이므로 FeedDataSource 개체 또는 심지어 Feeds 속성도 전달할 수 없습니다. 대신 FeedDataSource에 대한 참조를 App.xaml에 리소스로서 저장하고 이에 액세스합니다. 하지만 ItemsPage에 대해 DefaultViewModel을 초기화하려면 Feeds 속성을 사용해야 합니다. 여기에서는 Feeds Vector가 "Items"라는 이름의 키에 연결되며 ItemsPage에 대한 DefaultViewModel 멤버에 삽입됩니다. DefaultViewModelWindows::Foundation::Collections::IObservableMap입니다. 각 페이지에는 자체 DefaultViewModel이 있습니다. 일부 데이터로 DefaultViewModel을 초기화하면 ItemsViewSource::View 속성이 해당 데이터를 가리키게 됩니다. 컬렉션의 요소가 바인딩 가능하면 UI에 항목이 표시됩니다.

  2. ItemsPage.xaml.cpp에서 LoadState 메서드를 다음 코드로 바꿉니다.

    void ItemsPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
    {
        (void)sender;	// Unused parameter
        (void)e;	// Unused parameter
    
        // This is the first page to load on startup. The feedDataSource was constructed when the app loaded
        // in response to this declaration in app.xaml: <local:FeedDataSource x:Key="feedDataSource" />
        // and was initialized aynchronously in the OnLaunched event handler in app.xaml.cpp. 
        // Initialization might still be happening, but that's ok. 
        auto feedDataSource = safe_cast<FeedDataSource^>(App::Current->Resources->Lookup("feedDataSource"));
    
        // In ItemsPage.xaml (and every other page), the DefaultViewModel is set as DataContext:
        // DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
        // Because ItemsPage displays a collection of feeds, we set the Items element
        // to the FeedDataSource::Feeds collection. By comparison, the SplitPage sets this element to 
        // the Items collection of one FeedData object.
        this->DefaultViewModel->Insert("Items", feedDataSource->Feeds);
    
    }
    
    

이제 F5 키를 눌러 앱을 실행합니다. 템플릿 코드가 아직 변경되지 않고 항목 페이지에 전달된 일부 데이터가 그리드 칸에 이미 표시되어 있습니다. 다음과 같은 모양입니다. 단, 화면 해상도에 따라 항목이 다르게 배열될 수 있습니다.

b03.IC600948.png


항목 페이지에서 해야 할 마지막 작업은 사용자가 해당 항목 중 하나를 선택할 때 무엇을 할 것인지 지정하는 것입니다.

Hh465045.wedge(ko-kr,WIN.10).gif항목 페이지에서 분할 페이지로 이동하려면

  1. 사용자가 ItemsPage 컬렉션에서 블로그를 선택하면 항목 페이지에서 분할 페이지로 이동합니다. 이 탐색을 지원하기 위해, 선택한 항목 대신 GridView 항목이 단추처럼 작동하도록 하려고 합니다. GridView 항목이 단추처럼 응답하도록 하기 위해 다음 코드 블록에 표시된 대로 SelectionModeIsItemClickEnabled 속성을 설정합니다. 그런 다음 GridViewItemClicked 이벤트에 대한 처리기를 추가합니다. ItemsPage.xaml에서 itemGridView라는 GridView 요소에 대한 여는 태그를 찾습니다. 다음과 같습니다.

     <GridView
                x:Name="itemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.RowSpan="2"
                Padding="116,136,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionMode="None"
                IsItemClickEnabled="true"
                IsSwipeEnabled="false">
    
    
    

    이제, 커서를 닫는 대괄호 바로 앞에 놓고 Enter 키를 눌러 IsSwipeEnabled="false" 다음에 새 줄을 만듭니다. Margin="0,-10,0,10"을 입력한 다음 새 줄에서 ItemClick="itemGridView_ItemClick" 을 입력하기 시작합니다. ItemClick=을 입력할 때 자동 완성 기능에서 itemGridView_ItemClick이 제안됩니다. 자동 완성 항목을 선택하면 코드 숨김이 자동으로 스텁됩니다.

  2. ItemsPage.xaml.cpp에서 다음 include 지시문을 추가하여 SplitPage가 인식되도록 합니다.

    #include "SplitPage.xaml.h"
    
    
    
  3. 2단계의 설명대로 코드 숨김을 스텁하는 데 제안된 기본 이름을 사용했다면 itemGridView_ItemClick 이벤트 처리기 프로토타입이 ItemsPage.xaml.h에 나타나고 구현 스텁이 ItemsPage.xaml.cpp에 나타납니다. 다음 코드를 스텁 구현으로 붙여 넣으면 됩니다.

        // We must manually cast from Object^ to FeedData^.
        auto feedData = safe_cast<FeedData^>(e->ClickedItem);
    
        // Store the current URI so that we can look up the
        // correct feedData object on resume.
        App::Current->Resources->Insert("CurrentFeed", feedData);
    
        // Navigate to SplitPage and pass the title of the selected feed.
        // SplitPage will receive this in its LoadState method in the navigationParamter.
        this->Frame->Navigate(TypeName(SplitPage::typeid), feedData->Title);
    
    
    
  4. 항목 페이지가 분할 페이지로 이동하면 SplitPage::LoadState 메서드가 호출됩니다. ItemsPage에서와 마찬가지로 LoadState도 탐색 방법 및 앱의 이전 상태를 확인해야 합니다. 다음 예제의 코드 설명은 좀 더 자세한 정보를 제공합니다. App::OnLaunchedItemsPage::LoadState를 보면 상당 부분을 알 수 있을 것입니다.

    이제 SplitPage.xaml.cpp를 열고 이 코드를 사용해 전체 LoadState 메서드를 교체합니다.

    void SplitPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
    {
        // If we are navigating forward from ItemsPage, there is no existing page state.
        if (e->PageState == nullptr)
        {
            // Current feed was set in the click event in ItemsPage. We don't pass it in
            // through navigationParameter because on suspension, the default serialization 
            // mechanism will try to store that value but fail because it can only handle 
            // primitives, strings, and Guids.
            auto fd = safe_cast<FeedData^>(App::Current->Resources->Lookup("CurrentFeed"));
    
            // Insert into the ViewModel for this page to initialize itemsViewSource->View
            this->DefaultViewModel->Insert("Feed", fd);
            this->DefaultViewModel->Insert("Items", fd->Items);
    
            // When this is a new page, select the first item automatically unless logical page
            // navigation is being used (see the logical page navigation #region below).
            if (!UsingLogicalPageNavigation() && itemsViewSource->View != nullptr)
            {
                this->itemsViewSource->View->MoveCurrentToFirst();
            }
            else
            {
                this->itemsViewSource->View->MoveCurrentToPosition(-1);
            }
        }
    
        // e>PageState != null means either (1) we are returning from DetailPage
        // or (2) we are resuming from termination. If (1), then we still have our
        // state and don't have to do anything. If (2), then we have to restore the page.
        else if (!this->DefaultViewModel->HasKey("Feed"))
        {
            // All we stored is the Uri string for the feed, not the object. 
            auto uri = safe_cast<String^>(e->PageState->Lookup("Uri"));
    
            // FeedDataSource::InitDataSource has already been called. 
            // It's an asynchronous operation, so our FeedData might not
            // be available yet. GetFeedAsync uses a task_completion_event to  
            // wait (on its own thread) until the specified FeedData is available.
            // The next three methods follow the basic async pattern in C++:
            // 1. Call the async method.
            auto feedDataOp = FeedDataSource::GetFeedAsync(uri);
    
            // 2. Create a task from it.
            auto feedDataTask = create_task(feedDataOp);
    
            // 3. Define the work to be performed after the task completes.
            feedDataTask.then([this, e](FeedData^ feedData)
            {
                // Now we have the feedData, so it's safe to get the FeedItem
                // synchronously. Inserting into DefaultViewModel
                // initializes the itemsViewSource-View object.
                this->DefaultViewModel->Insert("Feed", feedData);
                this->DefaultViewModel->Insert("Items", feedData->Items);
    
                // DetailPage has to get the new Uri from this value.
                App::Current->Resources->Insert("CurrentFeed", feedData);
    
                // Now that we have a FeedData^, we can call GetFeedItem safely and
                // pass in the title that we stored before the app was terminated.
                auto itemTitle = safe_cast<String^>(e->PageState->Lookup("SelectedItem"));
                auto selectedItem = FeedDataSource::GetFeedItem(feedData, itemTitle);
    
                if (selectedItem != nullptr)
                {
                    this->itemsViewSource->View->MoveCurrentTo(selectedItem);
                }
            });
        }
    }
    
    
    

    앱을 종료 후 다시 시작하면 FeedItem 개체가 없고 문자열만 있습니다. 따라서 이 문자열을 사용하여 FeedItem을 조회해야 합니다. 하지만 FeedDataSource에서 FeedItem을 다운로드할 때까지는 그럴 수 없습니다. 그렇다고 모든 피드가 다운로드될 때까지 기다리고 싶지는 않습니다. 필요한 것을 사용할 준비가 될 때까지만 기다리고자 합니다. 이 모든 동기화가 LoadState 메서드 및 FeedDataSource::GetFeedAsync 메서드에서 발생합니다. FeedData 개체를 얻으면 GetFeedItem 메서드를 동기적으로 호출하여 선택한 게시물에 대한 FeedItem을 얻어 미리 보기 창을 채울 수 있습니다.

    참고  Items 속성을 분할 페이지에 대한 DefaultViewModel에 별도로 삽입하여 XAML 데이터 바인딩을 위해 항목에 액세스할 수 있도록 합니다.

    항목 페이지로 다시 이동하기 위해 추가 작업이 필요하지는 않습니다. 템플릿에는 BackButton.Click 이벤트를 처리하고 Frame.GoBack 메서드를 호출하는 코드가 포함되어 있습니다.

  5. 이제 F5 키를 눌러 앱을 실행하면 피드 데이터 항목을 클릭할 때 분할 페이지의 미리 보기 창에 나오는 블로그 텍스트가 원시 HTML로 표시됩니다. 이 문제를 해결하려면 선택된 블로그 게시물의 제목과 내용을 표시하는 데 사용된 레이아웃을 변경해야 합니다. 앱이 실행 중인 경우 Visual Studio로 돌아가서 Shift+F5를 눌러 앱을 닫으세요.

    경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
    어린이용이며, 설치가 필요없는 브라우저 게임입니다.
    https://s1004games.com

Hh465045.wedge(ko-kr,WIN.10).gifSplitPage::SaveState를 구현하려면

  • ItemsPage에서는 이미 모든 항목이 표시되기 때문에 페이지 상태를 저장할 필요가 없었습니다. 하지만 SplitPage에서는 현재 선택한 항목을 저장하여, 앱이 종료 후 다시 시작되는 경우 정확히 이 상태에서 시작되도록 해야 합니다. 앞에서 설명했듯이, SuspensionManager에서 직렬화를 수행하도록 할 것이므로 FeedItem 개체가 아니라 문자열과 숫자만 저장할 수 있습니다. 그러므로 현재 선택한 항목의 경우 FeedItem 개체의 제목과 FeedData 개체의 URI를 모두 저장하여, 종료 및 다시 시작 이후 이 항목을 다시 찾을 수 있도록 하겠습니다.

    SplitPage.xaml.cpp에서 SaveState 메서드를 찾고 다음 코드를 사용해 교체합니다.

    void SplitPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
        if (itemsViewSource->View != nullptr)
        {
            auto selectedItem = itemsViewSource->View->CurrentItem;
            if (selectedItem != nullptr)
            {
                auto feedItem = safe_cast<FeedItem^>(selectedItem);
                auto itemTitle = feedItem->Title;
                e->PageState->Insert("SelectedItem", itemTitle);
            }
    
            // Save the feed title also.
            auto feedData = safe_cast<FeedData^>(this->DefaultViewModel->Lookup("Feed"));
            e->PageState->Insert("Uri", feedData->Uri);
        }
    }
    
    

페이지에 대한 기능 추가를 마치려면 몇 가지 추가 변경을 수행해야 합니다. 이 코드를 추가한 후 스타일을 적용하고 애니메이션 효과를 줄 수 있습니다.

Hh465045.wedge(ko-kr,WIN.10).gifSplitPage 및 ItemsPage에서 바인딩과 레이아웃을 수정하려면

  1. 먼저, SplitPage.xaml에서 titlePanel이라는 Grid를 2개의 열에 걸치도록 변경합니다.

    <!-- Back button and page title -->
    <Grid x:Name="titlePanel" Grid.ColumnSpan="2">
    
    
  2. 다음으로, DefaultViewModel에 데이터를 추가할 때 "Feed"라는 키를 사용했기 때문에 Feed 속성에 바인딩하도록 페이지 제목의 바인딩을 변경해야 합니다. SplitPage.xaml에서 Feed.Title에 바인딩하도록 pageTitle이라는 TextBlock의 Text 바인딩을 다음과 같이 변경합니다.

    <TextBlock x:Name="pageTitle" Text="{Binding Feed.Title}" 
        Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" 
        IsHitTestVisible="false" TextWrapping="WrapWholeWords" 
        VerticalAlignment="Bottom" Margin="0,0,30,40"/>
    
    
  3. ItemsPage.xaml에서 AppName 키가 있는 고정 리소스에 페이지 제목이 바인딩됩니다. 다음과 같이 이 리소스의 텍스트를 Windows Team Blogs에 업데이트합니다.

    <x:String x:Key="AppName">Windows Team Blogs</x:String>
    
    

Hh465045.wedge(ko-kr,WIN.10).gifSplitPage.xaml에 WebView 컨트롤을 추가하려면

  1. SplitPage.xaml에서 선택된 블로그 게시물의 제목과 내용을 표시하는 데 사용된 레이아웃을 변경하려고 합니다. 그러기 위해 다음 레이아웃을 사용하여 itemDetail이라는 ScrollViewer를 교체합니다. 이 XAML의 상당 부분이 MainPage.xaml에 있는 이전 작업과 비슷하다는 것을 알 수 있습니다. Rectangle 요소의 목적은 이 문서의 뒷부분에 설명되어 있습니다.

    SplitPage.xaml에서 기존 ScrollViewer 요소를 축소하고 삭제한 후 다음 태그에 붙여 넣습니다.

            <!-- Details for selected item -->
            <ScrollViewer
                x:Name="itemDetail"
                AutomationProperties.AutomationId="ItemDetailScrollViewer"
                Grid.Column="1"
                Grid.RowSpan="2"
                Padding="70,0,120,0"
                DataContext="{Binding SelectedItem, ElementName=itemListView}"
                HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
                ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.VerticalScrollMode="Enabled"
                ScrollViewer.ZoomMode="Disabled">
    
                <Grid x:Name="itemDetailGrid" Margin="0,60,0,50">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
    
                    <Image Grid.Row="1" Margin="0,0,20,0" Width="180" Height="180" Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
                    <StackPanel x:Name="itemDetailTitlePanel" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2">
                        <TextBlock x:Name="itemTitle" Margin="0,-10,0,0" Text="{Binding Title}" Style="{StaticResource SubheaderTextBlockStyle}"/>
                        <TextBlock x:Name="itemSubtitle" Margin="0,0,0,20" Text="{Binding Subtitle}" Style="{StaticResource SubtitleTextBlockStyle}"/>
                    </StackPanel>
                 <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" Grid.Row="2" Grid.Column="1" Margin="0,15,0,20">
                        <Grid>
                            <WebView x:Name="contentView" />
                            <Rectangle x:Name="contentViewRect" />
                        </Grid>
                    </Border>
                </Grid>
            </ScrollViewer>
    
    
    
  2. SplitPage.xaml.cpp에서 ListView 선택이 변경되면 WebView를 업데이트하도록 하는 이벤트 처리 코드를 수정합니다. ItemListView_SelectionChanged 함수 서명과 구현이 이미 있습니다. 메서드 끝에 이러한 줄을 추가해야 합니다.

        // Sometimes there is no selected item, e.g. when navigating back
        // from detail in logical page navigation.
        auto fi = dynamic_cast<FeedItem^>(itemListView->SelectedItem);
        if(fi != nullptr)
        {
            contentView->NavigateToString(fi->Content);
        }    
    
    
    

Hh465045.wedge(ko-kr,WIN.10).gifDetailPage.xaml에 WebView 컨트롤을 추가하려면

  • DetailPage.xaml에서 제목 텍스트를 블로그 게시물 제목에 바인딩하고 WebView 컨트롤을 추가하여 블로그 페이지를 표시해야 합니다. 이렇게 하려면 뒤로 단추와 페이지 제목이 포함되어 있는 Grid를 이 GridWebView로 바꿉니다.

    <!-- Back button and page title -->
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <AppBarButton x:Name="backButton" Height="95" Margin="10,46,0,0"
                Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}" 
                AutomationProperties.Name="Back"
                AutomationProperties.AutomationId="BackButton"
                AutomationProperties.ItemType="Navigation Button"
                Icon="Back"/>
        <TextBlock x:Name="pageTitle" Text="{Binding Title}" 
                   Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" 
                   IsHitTestVisible="false" TextWrapping="NoWrap" 
                   VerticalAlignment="Bottom" Margin="0,0,30,40"/>
    </Grid>
    <Border x:Name="contentViewBorder" BorderBrush="Gray" BorderThickness="2" 
            Grid.Row="1" Margin="120,15,20,20">
        <WebView x:Name="contentView" />
    </Border>
    
    
    

Hh465045.wedge(ko-kr,WIN.10).gifDetailPage 클래스에서 LoadStateSaveState를 구현하려면

  1. DetailPage.xaml.h의 DetailPage 클래스에 다음 private 데이터 멤버를 추가합니다.

    private:
            Platform::String^ m_itemTitle;
            Platform::String^ m_feedUri;
    
    
    
  2. 다음의 using 문을 DetailPage.xaml.cpp에 추가합니다.

    e
    using namespace concurrency;
    
    
  3. DetailPage.xaml.cpp에서, 블로그 게시물로 이동하고 페이지의 DataContext를 설정하도록 LoadState 메서드 재정의에 코드를 추가합니다. SplitPage에서와 마찬가지로 앱의 이전 상태를 확인해야 합니다. 업데이트된 메서드는 다음과 같습니다.

    void DetailPage::LoadState(Object^ sender, Common::LoadStateEventArgs^ e)
    {
    	(void) sender;	// Unused parameter
    	(void) e;	// Unused parameter
    
        // Lookup the URL for the blog title that was either
        // (a) passed to us in this session or
        // (b) saved in the SaveState method when our app was suspended.
        m_itemTitle = safe_cast<String^>(e->NavigationParameter);
    
        // We are navigating forward from SplitPage
        if (e->PageState == nullptr)
        {
            auto feedData = safe_cast<FeedData^>(App::Current->Resources->Lookup("CurrentFeed"));
            m_feedUri = feedData->Uri;
    
            auto feedItem = FeedDataSource::GetFeedItem(feedData, m_itemTitle);
            if (feedItem != nullptr)
            {
                DefaultViewModel->Insert("Title", m_itemTitle);
                // Display the web page.
                contentView->Navigate(feedItem->Link);
            }
        }
    
        // We are resuming from suspension:
        else
        {
            // We are resuming, and might not have our FeedData object yet
            // so must get it asynchronously and wait on the result.
            String^ uri = safe_cast<String^>(e->PageState->Lookup("FeedUri"));
            auto feedDataOp = FeedDataSource::GetFeedAsync(uri); //URL
            auto feedDataTask = create_task(feedDataOp);
    
            feedDataTask.then([this, e](FeedData^ feedData)
            {
                App::Current->Resources->Insert("CurrentFeed", feedData);
    
                m_feedUri = feedData->Uri;
                m_itemTitle = safe_cast<String^>(e->PageState->Lookup("Item"));
                auto feedItem = FeedDataSource::GetFeedItem(feedData, m_itemTitle);
    
                if (feedItem != nullptr)
                {
                    DefaultViewModel->Insert("Title", m_itemTitle);
                    // Display the web page.
                    contentView->Navigate(feedItem->Link);
                }
            });
        }
    }
    
    
  4. LoadState와는 달리 SaveState는 단 두 줄의 코드로 구성됩니다. SplitPage에서와 마찬가지로, 앱을 종료 후 다시 시작하는 경우 FeedItem을 조회하려면 LoadState에 피드의 URI가 필요하므로 이를 저장해야 합니다. DetailPage.xaml.cpp에서 다음 코드를 사용하여 기존 SaveState 메서드를 바꿉니다.

    void DetailPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
    {
    	   (void) sender;	// Unused parameter
        // Store the itemTitle in case we are suspended or terminated.
        e->PageState->Insert("Item", m_itemTitle);
        e->PageState->Insert("FeedUri", m_feedUri);
    }
    
    
    
  5. F5 키를 눌러 앱이 빌드되는지 확인한 후 Visual Studio로 돌아가 Shift+F5를 눌러 빌드 실행을 중지합니다.

5부: 앱 바 추가

블로그 뷰어 앱에서 탐색의 많은 부분은 사용자가 항목 페이지와 분할 페이지 사이를 이동할 때, 그리고 사용자가 해당 페이지의 세부 내용을 찾아볼 때 발생합니다. 그러나 분할 페이지에서는 사용자가 블로그 게시물의 상세 보기에 액세스할 수 있는 방법도 제공해야 합니다. 페이지의 원하는 위치에 단추를 배치할 수도 있지만 이 경우 찾아보기 및 읽기에 방해가 될 수 있습니다. 대신 앱 바에 단추를 배치하겠습니다. 이 단추는 숨겨져 있다가 사용자가 원하는 경우에만 표시됩니다. 이 섹션에서는 세부 정보 페이지로 이동하는 단추가 있는 앱 바를 추가하겠습니다.

앱 바는 기본적으로 숨겨져 있는 UI이며 사용자가 화면 가장자리를 밀거나 마우스 오른쪽 단추를 클릭하면 표시되거나 사라집니다. 앱 바는 사용자에게 탐색 UI, 명령, 도구를 제공합니다. 앱 바를 페이지 맨 위, 맨 아래 또는 두 곳 모두에 표시할 수 있습니다. 맨 위 앱 바에는 탐색을, 맨 아래 앱 바에는 도구 및 명령을 배치하는 것이 좋습니다.

XAML에서 앱 바를 추가하려면, PageTopAppBar 또는 BottomAppBar 속성에 AppBar 컨트롤을 할당한 다음 AppBarAppBarButton 요소를 포함합니다.

Hh465045.wedge(ko-kr,WIN.10).gif분할 페이지 앱 바에 단추를 추가하려면

  1. SplitPage.xaml에서 닫는 Page.Resources 태그 다음에 다음 태그를 붙여 넣어 앞쪽을 가리키는 AppBarButton이 포함된 탐색 모음을 만듭니다.

    <Page.TopAppBar>
        <AppBar Padding="10,0,10,0">
            <Grid>
               <AppBarButton x:Name="fwdButton" Height="95" Margin="150,46,0,0"
                   Command="{Binding NavigationHelper.GoForwardCommand, ElementName=pageRoot}" 
                   AutomationProperties.Name="Forward"
                   AutomationProperties.AutomationId="ForwardButton"
                   AutomationProperties.ItemType="Navigation Button"
                   HorizontalAlignment="Right"
                   Icon="Forward"/>
             </Grid>
         </AppBar>
    </Page.TopAppBar>
    
    
  2. 이제 Icon 특성 다음, /> 앞에 공백과 Click="fwdButton_Click"을 순서대로 입력합니다. 자동 완성 기본값을 그대로 사용해도 됩니다. 이렇게 하면 코드 숨김 페이지에 이벤트 처리기가 만들어집니다.

Hh465045.wedge(ko-kr,WIN.10).gif세부 정보 페이지에 탐색을 추가하려면

  1. SplitPage.xaml.cpp에 다음 #include 지시문을 추가합니다.

    #include "DetailPage.xaml.h"
    
    
    
  2. SplitPage.xaml.cpp에서 이 코드를 fwdButton_Click 메서드 본문에 추가합니다.

    // Navigate to the appropriate destination page, and configure the new page
    // by passing required information as a navigation parameter.
    
    auto selectedItem = dynamic_cast<FeedItem^>(this->itemListView->SelectedItem);
    
    // selectedItem will be nullptr if the user invokes the app bar
    // and clicks on "view web page" without selecting an item.
    if (this->Frame != nullptr && selectedItem != nullptr)
    {
        auto itemTitle = safe_cast<String^>(selectedItem->Title);
        this->Frame->Navigate(TypeName(DetailPage::typeid), itemTitle);
    }
    
    
    

이제 핵심 앱 기능이 완료되었습니다! 앱을 빌드하고 실행하여 페이지 간의 이동을 테스트합니다. 항목 페이지에서 항목을 선택하면 앱이 분할 페이지로 이동해야 합니다. 분할 페이지에서는 뒤로 단추를 사용하여 항목 페이지로 돌아가는지 테스트하고 화면 위쪽에서 살짝 밀거나 마우스 오른쪽 단추를 클릭하여 앱 바를 호출한 다음 단추를 누르거나 클릭하여 세부 정보 페이지로 이동하는지 테스트합니다. 세부 정보 페이지에서 뒤로 단추를 사용하여 분할 페이지로 돌아가는지 테스트합니다. 분할 페이지에서 다른 항목을 선택하면 해당 항목의 미리 보기가 나타나야 합니다. 다음에는 UI의 모양을 개선하고 세로 방향 지원을 추가하겠습니다.

6부: 애니메이션 및 전환 추가

애니메이션이라고 하면 우리는 대개 여러 개체가 화면 이곳저곳을 움직이는 장면을 떠올립니다. 그러나 XAML에서 애니메이션은 개체에 대한 속성 값을 변경할 수 있는 방법입니다. 이렇게 하면 애니메이션이 이리저리 움직이는 개체 이외의 다양한 용도로 유용하게 됩니다. 블로그 뷰어 앱에서는 Windows.UI.Xaml.Media.Animation 네임스페이스에서 일부 기본 제공 애니메이션 및 방향 논리를 사용하여 다양한 레이아웃과 방향에 맞춰 UI를 조정합니다.

테마 애니메이션 추가

테마 애니메이션은 미리 구성된 애니메이션입니다. 예를 들어 PopInThemeAnimation을 사용하면 페이지가 로드될 때 보기가 오른쪽에서 왼쪽으로 이동합니다. FromHorizontalOffset 속성의 값을 늘리면 효과가 보다 현저하게 나타납니다. 여기서는 DetailPage.xaml의 StoryboardPopInThemeAnimation을 배치하고 이를 리소스로 만듭니다. 그런 다음 애니메이션의 대상을 웹 콘텐츠를 둘러싼 Border로 설정합니다. 이는 Border 및 그 안의 모든 요소에 애니메이션 효과를 줍니다.

Hh465045.wedge(ko-kr,WIN.10).gif세부 정보 페이지에 테마 애니메이션을 추가하려면

  1. 다음 XAML 코드 조각을 DetailPage.xaml의 Page.Resources 노드에 붙여 넣습니다.

    <Storyboard x:Name="PopInStoryboard">
        <PopInThemeAnimation  Storyboard.TargetName="contentViewBorder" FromHorizontalOffset="400"/>
    </Storyboard>
    
    
    
  2. 다음 코드를 DetailPage.xaml.cpp의 DetailPage::LoadState 메서드 시작 부분에 붙여 넣습니다. LoadState의 재정의는 기본 클래스 LayoutAwarePage에서 OnNavigatedTo 메서드에 의해 호출됩니다.

    // Run the PopInThemeAnimation. 
    Windows::UI::Xaml::Media::Animation::Storyboard^ sb = dynamic_cast<Windows::UI::Xaml::Media::Animation::Storyboard^>(this->FindName("PopInStoryboard"));
    if (sb != nullptr)
    {
        sb->Begin();
    }    
    //... rest of method as before
    
    
    

테마 전환 추가

테마 전환은 애니메이션 세트와 UI 요소에 연결할 수 있는 미리 패키지된 동작에 결합된 Storyboard를 말합니다. ContentThemeTransitionContentControl과 함께 사용되며 컨트롤의 콘텐츠가 변경되면 자동으로 트리거됩니다.

앱에서 분할 페이지 목록 보기의 게시물 제목을 저장하는 TextBlock에 테마 전환을 추가해 보겠습니다. TextBlock의 콘텐츠가 변경되면 ContentThemeTransition이 자동으로 트리거되고 실행됩니다. 애니메이션은 미리 정의되므로 실행하기 위해 아무 작업도 수행할 필요가 없습니다. TextBlock에 연결하기만 하면 됩니다.

Hh465045.wedge(ko-kr,WIN.10).gifSplitPage.xaml에 테마 전환을 추가하려면

  • SplitPage.xaml에서 pageTitle이라는 TextBlock은 빈 요소 태그입니다. TextBlock에 테마 전환을 포함하기 때문에 여는 태그와 닫는 태그를 포함하도록 TextBlock을 변경해야 합니다. 기존 태그를 다음 태그로 바꿉니다.

    <TextBlock x:Name="pageTitle" Grid.Column="1" Text="{Binding Feed.Title}" Style="{StaticResource HeaderTextBlockStyle}"
        IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40">
        <TextBlock.Transitions>
            <TransitionCollection>
                <ContentThemeTransition />
             </TransitionCollection>
        </TextBlock.Transitions>
    </TextBlock>
    
    
    

    TextBlock의 콘텐츠가 변경되면 ContentThemeTransition이 자동으로 트리거되고 실행됩니다. 애니메이션은 미리 정의되므로 실행하기 위해 아무 작업도 수행할 필요가 없습니다. TextBlock에 연결하기만 하면 됩니다. 자세한 내용 및 테마 애니메이션과 전환 목록은 빠른 시작: 애니메이션을 참조하세요.

6부: 스타일을 사용하여 일관된 모양 만들기

사용자가 Windows 팀 블로그 웹 사이트와 블로그 뷰어 앱 사이를 원활하게 이동할 수 있게 하려고 합니다. 새로운 Windows UI의 기본적인 어두운 테마는 Windows 팀 블로그 웹 사이트와 잘 맞지 않습니다. 아래와 같이 실제 블로그 페이지를 WebView로 로드하는 세부 정보 페이지를 보면 가장 명확히 알 수 있습니다.

b04.IC600949.png


필요에 따라 업데이트할 수 있는 일관된 모양을 앱에 지정하려면 브러시와 스타일을 사용합니다. Brush를 사용하면 한곳에서 모양을 정의한 후 필요할 때마다 이 모양을 사용할 수 있습니다. Style을 사용하여 컨트롤의 속성 값을 설정하고 해당 설정을 앱 전반에서 재사용할 수 있습니다.

자세히 살펴보기 전에 브러시를 사용하여 앱에 있는 페이지의 배경색을 설정하는 방법을 알아보겠습니다. 앱의 각 페이지에는 페이지의 배경색을 정의하는 Background 속성이 설정된 루트 Grid가 있습니다. <Grid Background="Blue">에서와 같이, 각 페이지의 배경을 개별적으로 설정할 수 있습니다. 그러나 Brush를 리소스로 정의한 다음 이를 사용하여 모든 페이지의 배경색을 정의하는 것이 더 좋습니다. 이것이 Visual Studio 템플릿의 작업 방식입니다. <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

GridBackground 속성을 설정하기에 좋지만, 일반적으로 원하는 모양을 얻으려면 속성을 두 개 이상 설정해야 합니다. 원하는 수의 속성에 대해 설정을 Style로 그룹화하고 이 Style을 컨트롤에 적용할 수 있습니다.

리소스가 정의되는 위치에 따라 리소스를 사용할 수 있는 범위가 결정됩니다. 페이지 수준 XAML 파일, 앱 수준 App.xaml 파일 또는 별도 리소스 사전 XAML 파일에서 리소스를 정의할 수 있습니다. 앱 간에 리소스 사전 XAML 파일을 공유할 수 있으며, 여러 리소스 사전을 하나의 앱으로 병합할 수 있습니다.

블로그 뷰어 앱에서는 리소스를 전체 앱에서 사용할 수 있도록 App.xaml에서 정의합니다. 또한 XAML 파일에는 개별 페이지에 대한 일부 리소스가 정의되어 있습니다. App.xaml과 페이지에 모두 동일한 키를 사용하는 리소스가 정의되어 있는 경우 페이지의 리소스가 App.xaml의 리소스보다 우선합니다. 마찬가지로 App.xaml에 정의된 리소스는 별도의 리소스 사전 파일에 정의된 동일한 키 리소스보다 우선합니다. 자세한 내용은 빠른 시작: 컨트롤 스타일 지정을 참조하세요.

Style 정의에서는 TargetType 특성과 하나 이상의 Setter 요소가 있어야 합니다. TargetTypeStyle이 적용되는 유형을 지정하는 문자열(이 경우 Panel)로 설정합니다. StyleTargetType 특성과 일치하지 않는 컨트롤에 적용하려고 하면 예외가 발생합니다. 각 Setter 요소에는 PropertyValue가 필요합니다. 이 설정은 설정이 적용되는 컨트롤 속성과 해당 속성에 설정할 값을 지정합니다.

페이지의 Background를 변경하기 위해 ApplicationPageBackgroundThemeBrush를 고유의 사용자 지정 브러시로 바꿉니다. 사용자 지정 브러시에 대해 Color #FF0A2562를 설정합니다. 이 색상은 Windows 팀 블로그 웹 사이트의 색과 잘 맞는 파란색입니다. 시스템 테마 브러시를 바꾸기 위해 LayoutRootStyle을 기반으로 하는 Style을 만든 다음 Background 속성을 변경합니다.

Hh465045.wedge(ko-kr,WIN.10).gif모든 페이지에 대한 배경색을 설정하려면

  1. 레이아웃 루트에 새 스타일을 정의하려면 이러한 브러시 및 스타일 정의를 App.xaml의 <Application.Resources> 노드에 붙여 넣습니다.

      <SolidColorBrush x:Key="WindowsBlogBackgroundBrush" Color="#FF0A2562"/>
    
      <Style x:Key="WindowsBlogLayoutRootStyle" TargetType="Panel">
          <Setter Property="Background" Value="{StaticResource WindowsBlogBackgroundBrush}"/>
      </Style>
    
    
  2. 앱의 각 페이지에 있는 루트 Grid 요소는 현재 Background 특성을 사용하여 페이지 배경색을 설정합니다. ItemsPage.xaml, SplitPage.xaml, DetailPage.xaml에서 이 요소를 변경하여 App.xaml에 추가한 WindowsBlogLayoutRootStyle을 참조하도록 만들어 모든 페이지가 동일한 배경을 사용하도록 만들겠습니다.

     <Grid Style="{StaticResource WindowsBlogLayoutRootStyle}">
    
    
  3. F5 키를 눌러 앱을 빌드 및 실행한 후 파란색 페이지를 검토합니다.

컨트롤 및 데이터 템플릿

앱에 Windows 팀 블로그 웹 사이트의 모양과 느낌을 더 주려면 BrushesStyles 외에 사용자 지정 데이터 템플릿을 사용합니다.

Hh465045.wedge(ko-kr,WIN.10).gif날짜에 대한 컨트롤 템플릿을 추가하려면

  • App.xaml의 <Application.Resources> 노드에서 날짜를 표시하는 정사각형 블록을 정의하는 ControlTemplate을 추가합니다. ItemsPage.xaml과 SplitPage.xaml 모두에서 참조할 수 있도록 App.xaml에서 정의합니다.

     <ControlTemplate x:Key="DateBlockTemplate">
        <Canvas Height="86" Width="86"  Margin="8,8,0,8" 
            HorizontalAlignment="Left" VerticalAlignment="Top">
             <TextBlock TextTrimming="WordEllipsis" TextWrapping="NoWrap" 
                 Width="Auto" Height="Auto" Margin="8,0,4,0" 
                 FontSize="32" FontWeight="Bold">
                 <TextBlock.Text>
                     <Binding Path="PubDate" 
                     Converter="{StaticResource dateConverter}"
                         ConverterParameter="month"/>
                 </TextBlock.Text>
             </TextBlock>
    
             <TextBlock TextTrimming="WordEllipsis" TextWrapping="Wrap" 
                 Width="40" Height="Auto" Margin="8,0,0,0"
                 FontSize="34" FontWeight="Bold" Canvas.Top="36">
                 <TextBlock.Text>
                     <Binding Path="PubDate" 
                         Converter="{StaticResource dateConverter}"
                         ConverterParameter="day"/>
                 </TextBlock.Text>
             </TextBlock>
             <Line Stroke="White" StrokeThickness="2" 
                 X1="54" Y1="46" X2="54" Y2="80"/>
    
             <TextBlock TextWrapping="Wrap" 
                 Width="20" Height="Auto" 
                 FontSize="{StaticResource ControlContentThemeFontSize}"
                 Canvas.Top="42" Canvas.Left="60">
                 <TextBlock.Text>
                     <Binding Path="PubDate"
                     Converter="{StaticResource dateConverter}"
                     ConverterParameter="year"/>
                 </TextBlock.Text>
             </TextBlock>
         </Canvas>
    </ControlTemplate>
    
    
    

    ControlTemplate은 이전에 DateConverter에서 만든 Convert 함수에 전달되는 "일", "월", "연도" 등의 매개 변수를 정의합니다. 일, 월, 연도를 독립된 요소로서 취급하여 서로 다른 글꼴 크기로 서식을 지정하기 때문에 날짜의 각 부분을 별도로 반환하기 위해 Convert 메서드에서 새 기능을 작성해야 했습니다.

Hh465045.wedge(ko-kr,WIN.10).gif항목 페이지에 데이터 템플릿을 추가하려면

  1. ItemsPage.xaml에서 이러한 요소를 <Page.Resources> 노드에 추가하여 그리드 항목의 모양을 정의합니다.

      <!-- light blue -->
            <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF557EB9"/>
    
            <!-- Grid Styles -->
            <Style x:Key="GridTitleTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
                <Setter Property="FontSize" Value="26.667"/>
                <Setter Property="Margin" Value="12,0,12,2"/>
            </Style>
    
            <Style x:Key="GridDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
                <Setter Property="VerticalAlignment" Value="Bottom"/>
                <Setter Property="Margin" Value="12,0,12,60"/>
            </Style>
    
            <DataTemplate x:Key="DefaultGridItemTemplate">
                <Grid HorizontalAlignment="Left" Width="250" Height="250"
                    Background="{StaticResource BlockBackgroundBrush}" >
                    <StackPanel>
                        <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitleTextStyle}" Margin="5,5,5,5" />
                        <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" Margin="5,5,5,5" />                    
                    </StackPanel>
                    <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal"
                            Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
                        <TextBlock Text="Last Updated" Margin="12,4,0,8" Height="42"/>
                        <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}" Margin="12,4,12,8" Height="42"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>
    
        
    
    
    
  2. 또한 ItemsPage.xaml에서 itemGridView라는 GridView 요소를 업데이트하여 이 요소가 방금 추가했지만 인라인 템플릿을 사용하지 않는 DefaultGridItemTemplate 리소스를 참조하도록 합니다. 전체 GridView 요소(여는 태그에서 닫는 태그까지)를 자체적으로 닫히는 단일 태그만으로 구성된 이 요소로 바꿉니다.

     <!-- Horizontal scrolling grid -->
            <GridView
                x:Name="itemGridView"
                AutomationProperties.AutomationId="ItemsGridView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.RowSpan="2"
                Padding="116,136,116,46"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                SelectionMode="None"
                ItemTemplate="{StaticResource DefaultGridItemTemplate}"
                IsItemClickEnabled="true"
                IsSwipeEnabled="false"
                ItemClick="itemGridView_ItemClick">
            </GridView>
    
    
    

    새로운 데이터 템플릿에 대한 참조가 이 줄에 있습니다. ItemTemplate="{StaticResource DefaultGridItemTemplate}".

Hh465045.wedge(ko-kr,WIN.10).gif분할 페이지에 데이터 템플릿을 추가하려면

  1. SplitPage.xaml에서 다음 요소를 <Page.Resources> 노드에 추가하여 목록 항목의 모양을 정의합니다. 두 개의 데이터 템플릿이 있으며 하나는 가로 모드용, 하나는 세로 모드용입니다.

    <!-- Green    -->
    <SolidColorBrush x:Key="BlockBackgroundBrush" Color="#FF6BBD46"/>
    
    <DataTemplate x:Name="LandscapeItemTemplate">
        <Grid HorizontalAlignment="Stretch" Width="Auto" Height="110" Margin="10,10,10,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- Green date block -->
            <Border Background="{StaticResource BlockBackgroundBrush}" Width="110" Height="110" />
            <ContentControl Template="{StaticResource DateBlockTemplate}" />
            <StackPanel Grid.Column="1"  HorizontalAlignment="Left"  Margin="12,8,0,0">
                <TextBlock Text="{Binding Title}" FontSize="26.667" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" />
                    <TextBlock Text="{Binding Author}" FontSize="18.667" />
            </StackPanel>
        </Grid>
    </DataTemplate>
    
    <DataTemplate x:Name="PortraitItemTemplate">
        <Grid HorizontalAlignment="Stretch" Width="Auto" Height="110" Margin="10,10,10,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
             </Grid.ColumnDefinitions>
             <!-- Green date block -->
             <Border Background="{StaticResource BlockBackgroundBrush}" Width="110" Height="110" />
             <ContentControl Template="{StaticResource DateBlockTemplate}" />
             <StackPanel Grid.Column="1"  HorizontalAlignment="Left"  Margin="12,8,0,0">
                 <TextBlock Text="{Binding Title}" FontSize="26.667" TextWrapping="Wrap" MaxHeight="72" Foreground="#FFFE5815" />
                 <TextBlock Text="{Binding Author}" FontSize="18.667" />
             </StackPanel>
         </Grid>
    </DataTemplate>
    
    
    
  2. 다음으로 SplitPage.xaml에서 itemListView라는 ListView 요소의 ItemTemplate 속성을 업데이트하여, 장치 방향이 가로 모드인지 세로 모드인지에 따라 두 개의 사용자 지정 데이터 템플릿 중 하나를 사용하도록 만들어야 합니다. 여기서는 XAML로 기본 템플릿을 지정하고 나중에 VisualState XAML 요소와 몇 가지 코드 숨김을 사용하여 세로 모드 템플릿으로 전환하고 방향 변경을 검색하는 방법을 확인합니다.

    SplitPage.xaml에서 기존 ListView 요소를 이 요소로 바꿉니다.

             <!-- Vertical scrolling item list -->
            <ListView
                x:Name="itemListView"
                AutomationProperties.AutomationId="ItemsListView"
                AutomationProperties.Name="Items"
                TabIndex="1"
                Grid.Row="1"
                Margin="-10,-10,0,0"
                Padding="120,0,0,60"
                ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
                IsSwipeEnabled="False"
                SelectionChanged="ItemListView_SelectionChanged"
                ItemTemplate="{StaticResource LandscapeItemTemplate}">
    
                <ListView.ItemContainerStyle>
                    <Style TargetType="FrameworkElement">
                        <Setter Property="Margin" Value="0,0,0,10"/>
                    </Style>
                </ListView.ItemContainerStyle>
            </ListView>
    
    
    

지금까지 연습한 새 스타일이 적용된 앱은 Windows 팀 블로그 웹 사이트의 모양과 느낌에 잘 어울립니다.

b05.IC600950.png


b06.IC618543.png


b07.IC600952.png


다른 스타일을 기준으로 하여 스타일을 사용하면 앱의 여러 가지 모양을 빠르게 정의하고 적용할 수 있습니다. 다음 섹션에서는 애니메이션과 스타일에 대해 아는 내용을 종합하여, 앱이 실행 중에 다양한 레이아웃과 방향에 맞춰 유연하게 조정되도록 하는 방법을 알아봅니다.

7부: 다양한 레이아웃에 맞게 조정

Windows 스토어 앱용 Visual Studio 템플릿에는 표시 방향 변경을 처리하는 코드가 포함되어 있지 않습니다. 따라서 해당 코드를 직접 작성해야 합니다.

XAML을 사용하여 여러 보기 간에 전환하려면 VisualStateManger를 사용하여 각 페이지에 대해 VisualStateGroup을 정의합니다. 각 VisualStateGroupVisualState 정의가 하나 이상 포함됩니다. 런타임에 SizeChanged 이벤트 처리기에서 VisualStateManager::GoToState를 호출하여 페이지가 지정 VisualState를 사용하여 UI를 렌더링하도록 합니다. 이 앱에서 항목 페이지와 분할 페이지 각각은 기본값인 가로 방향 및 세로 방향에 해당하는 두 개의 VisualStateGroups를 사용합니다. VisualState는 본래 XAML 요소에 대한 변형 집합입니다. 각 VisualState에는 변경할 페이지 요소를 지정하는 애니메이션이 있습니다.

이 자습서의 스타일을 사용하여 일관된 모양 만들기 섹션에서 앱 모양을 사용자 지정하는 스타일 및 템플릿을 만들었습니다. 이러한 스타일과 템플릿은 기본 가로 뷰에서 사용됩니다. 세로 모드에서 사용자 지정 모양을 유지하려면 해당 보기에 대한 일부 사용자 지정 스타일 및 템플릿도 만들어야 합니다.

Hh465045.wedge(ko-kr,WIN.10).gifItemsPage.xaml에서 세로 모드로 적응하려면

  1. 여기서 디자인 결정을 해야 합니다. 항목을 가로 모드로 표시하려면 GridView를 사용합니다. GridView는 가로로 스크롤하도록 디자인되었습니다. 항목을 세로 모드에서 세로로 스크롤하려면 ListView를 사용해야 합니다. 하지만 이 경우 앱은 런타임에 두 개의 별도의 컨트롤을 로드해야 하므로, 로드 시간과 메모리 공간이 늘어날 수 있습니다. 테스트를 해야만 추가 컨트롤을 보유하는 비용이 큰지 여부를 알 수 있습니다. 이 앱에서는 간단히 하기 위해 두 방향에 모두 동일한 GridView를 사용하겠습니다. 여전히 세로 방향으로 세로 스크롤을 사용하지만, 항목의 폭을 약간 좁혀서 세로 직사각형에 더 맞는 비율로 만들 수 있습니다. 이때 일부 글꼴 크기도 조정하여 제목에서 과도하게 텍스트 줄 바꿈이 발생하지 않도록 만들어야 합니다. 이렇게 변경하기 위해 VisualState를 정의하고 여기에 애니메이션을 채워 각각의 특정 요소를 수정합니다.

    ItemsPage.xaml에서 <Page.Resources> 노드에 다음 스타일을 추가합니다. 그러면 비교적 폭이 좁은 직사각형에 더 맞도록 항목 제목의 글꼴이 더 작아집니다. 이 스타일은 다음 단계에서 추가할 데이터 템플릿에서 사용합니다.

    <Style x:Key="GridTitlePortraitTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
        <Setter Property="FontSize" Value="20"/>
        <Setter Property="Margin" Value="12,0,12,2"/>
    </Style>
    
    
  2. 이제 이 DataTemplate<Page.Resources> 노드에 추가하여 GridView에서 세로 방향으로 사용할 템플릿을 정의합니다.

    <DataTemplate x:Key="PortraitGridItemTemplate">
        <Grid HorizontalAlignment="Left" Width="160" Height="250"
        Background="{StaticResource BlockBackgroundBrush}">
            <StackPanel>
                <TextBlock Text="{Binding Title}" Style="{StaticResource GridTitlePortraitTextStyle}" Margin="5,5,5,5"/>
                <TextBlock Text="{Binding Description}" Style="{StaticResource GridDescriptionTextStyle}" Margin="5,5,5,5" TextTrimming="CharacterEllipsis"/>
            </StackPanel>
            <StackPanel VerticalAlignment="Bottom" 
                Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
                <TextBlock Text="Last Updated:"/>
                <TextBlock Text="{Binding PubDate, Converter={StaticResource dateConverter}}"/>
            </StackPanel>
        </Grid>
    </DataTemplate> 
    
    
  3. 그럼, 페이지에 새 데이터 템플릿을 적용하겠습니다. 여기서 VisualState가 필요하게 됩니다. ItemsPage.xaml에서 루트 Grid 요소의 닫는 태그 바로 위에 이 코드를 추가합니다.

     <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="ViewStates">
            <VisualState x:Name="DefaultLayout"/>
            <VisualState x:Name="Portrait">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" Storyboard.TargetProperty="ItemTemplate">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PortraitGridItemTemplate}"/>
                    </ObjectAnimationUsingKeyFrames>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemGridView" Storyboard.TargetProperty="Padding">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="40,136,116,40"/>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    

    잠시 이 XAML을 살펴보겠습니다. 우리는 VisualStateManager.VisualStateGroups 요소를 추가했고 여기에 ViewStates 그룹을 중첩했습니다. 이 그룹에는 가로 모드에 대한 "DefaultLayout"이라는 상태와 세로 모드에 대한 "Portrait"라는 상태 등 두 가지의 상태가 있습니다. 여기서 "DefaultLayout"이 비어 있습니다. 페이지에 대한 XAML과 동일하다는 것을 의미합니다. 장치가 가로 방향일 때 페이지가 SizeChanged 이벤트를 수신하면 페이지 XAML을 사용하여 렌더링합니다. 장치가 세로 방향일 때는 페이지가 기본 XAML을 로드한 다음 애니메이션에서 지정된 변경을 수행합니다. GridView에는 다른 항목 템플릿이 사용되므로, 세로 방향의 세로 크기에 더 잘 맞도록 안쪽 여백도 약간 조정하겠습니다.

  4. 이제 요소의 마지막 특성으로서 SizeChanged를 입력하여 SizeChanged 이벤트를 루트 요소 Page에 추가합니다. 입력하기 시작하면 UI에서 식을 완료하도록 추천 내용을 제공하고 이벤트 처리기를 만들도록 지원합니다. UI 추천 내용을 사용하는 경우 메서드 서명 및 구현을 수동으로 추가할 필요가 없습니다. 이벤트 처리기를 만들려면 pageRoot_SizeChanged를 입력하기 시작한 후 해당 이벤트 이름과 일치하면 UI 추천 내용을 수락합니다.
  5. 이제, 장치 방향이 바뀌면 적절한 VisualState를 호출하도록 코드 숨김을 추가해야 합니다. ItemsPage.xaml.cpp에서 다음 코드를 빈 pageRoot_SizeChanged 메서드에 추가합니다.
    if (e->NewSize.Height / e->NewSize.Width >= 1)
    {
        VisualStateManager::GoToState(this, "Portrait", true);
    }
    else
    {
        VisualStateManager::GoToState(this, "DefaultLayout", true);
    }
    
    

Hh465045.wedge(ko-kr,WIN.10).gifSplitPage.xaml에서 세로 모드로 적응하려면

  1. 세로 모드에서 분할 페이지는 두 행 각각을 논리 페이지로 처리합니다. 사용자가 목록을 보고 있으면 전체 화면을 차지하고 세부 정보 열이 축소됩니다. 사용자가 항목을 선택하면 목록 열이 축소되고 세부 정보 열이 화면을 차지합니다. 따라서 SplitPage.xaml에는 세로 모드용으로 두 개의 VisualStates가 필요합니다. SinglePane은 목록 측만 표시하고 SinglePane_Detail은 세부 정보 측을 표시합니다. 각 애니메이션에 대해 대상, 대상 속성, 이 속성에 할당되는 값을 확인합니다. 이 VisualStates는 기본적으로 SplitPage.xaml에 포함되어 있으며 우리는 이 앱에서는 변경하지 않겠습니다. 또한 #pragma region Logical page navigation을 확인하여 페이지 논리가 세로 모드에서 호출할 VisualState를 결정하는 방법을 알아보세요.

    화면 너비가 768픽셀일 때도 SplitPage가 예상대로 동작하도록 이 코드에서 한 가지를 변경하겠습니다. MinimumWidthForSupportingTwoPanes가 768로 정의되어 있으면 UsingLogicalPageNavigation 멤버 함수가 false를 반환합니다. 이 해상도에서도 논리적 페이지 탐색을 호출하려고 합니다. 따라서 다음 코드 조각에 나온 대로 함수의 < 연산자를 <=으로 변경하거나 SplitPage.xaml.h에서 MinimumWidthForSupportingTwoPanes의 정의를 769로 변경해야 합니다.

    bool SplitPage::UsingLogicalPageNavigation()
    {
    	return Windows::UI::Xaml::Window::Current->Bounds.Width <= MinimumWidthForSupportingTwoPanes;
    }
    
    
  2. 의외로 SplitPage.xaml은 기본적으로 SizeChanged 이벤트 처리기를 지정하지 않기 때문에, 이를 지금 직접 지정해야 합니다. 루트 Page 요소의 여는 태그에서 SizeChanged 입력을 시작한 다음 자동 완성을 사용하여 문을 완료하고 코드 숨김을 스텁합니다. 그런 다음 SplitPage.xaml.cpp에서 다음 코드를 빈 메서드 스텁에 추가합니다.
    if (e->NewSize.Height / e->NewSize.Width >= 1)
    {
        VisualStateManager::GoToState(this, "SinglePane", true);
    }
    else
    {
        VisualStateManager::GoToState(this, "PrimaryView", true);
    }
    
    

Hh465045.wedge(ko-kr,WIN.10).gif세로 모드에서 WebView 여백을 조정하려면

  1. DetailPage.xaml에서 모든 사용 가능한 공간을 사용하도록 세로 모드인 WebView의 여백을 조정하면 됩니다. 이렇게 하려면 두 개의 VisualStates를 추가하고 "세로" 상태에서 애니메이션을 추가하여 contentViewBorder에서 Margin 속성 값을 변경합니다. 마지막 닫는 Grid 태그 바로 앞에 다음 요소를 붙여 넣습니다.

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="ViewStates">
            <VisualState x:Name="Landscape"/>
            <VisualState x:Name="Portrait">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentViewBorder" Storyboard.TargetProperty="Margin">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="20,5,20,20"/>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    
    
  2. DetailPage.xaml에서 루트 Page 요소에 SizeChanged 이벤트를 추가합니다. 앞에서와 마찬가지로 SizeChanged="pageRoot_SizeChanged" 입력을 시작한 다음 자동 완성을 사용하여 코드 숨김을 스텁합니다.
  3. DetailPage.xaml.cpp에서 비어 있는 새 pageRoot_SizeChanged 메서드에 다음 코드를 추가합니다.
    if (e->NewSize.Height / e->NewSize.Width >= 1)
    {
        VisualStateManager::GoToState(this, "Portrait", true);
    }
    else
    {
        VisualStateManager::GoToState(this, "Landscape", true);
    }
    
    

추가 작업: 시작 화면 추가

앱이 주는 첫인상은 사용자가 앱을 시작할 때 표시되는 시작 화면에서 나옵니다. 앱이 리소스를 초기화하는 동안 시작 화면은 사용자에게 앱이 작동 중임을 확인시켜 주고 앱의 첫 페이지로 대체할 준비가 되었을 때 사라집니다.

시작 화면은 배경색과 이미지로 구성됩니다. 앱에서 우리는 프로젝트 템플릿에서 제공된 기본 배경과 이미지(SplashScreen.png)를 사용했습니다. 하지만 원하는 경우 매니페스트 편집기의 응용 프로그램 UI 탭을 사용하여 Package.appxmanifest의 설정을 변경하여 색과 이미지를 변경할 수 있습니다.

다음은 블로그 뷰어 시작 화면의 한 가지 예입니다.

b08.IC561675.png


SplashScreen 클래스의 속성과 메서드를 사용하여 시작 화면을 확장할 수도 있습니다. 예를 들어 시작 화면의 좌표를 가져온 다음 이 좌표를 사용하여 앱의 첫 페이지를 배치할 수 있습니다. 또한 시작 화면이 닫히는 때를 확인하여 앱의 콘텐츠 개시 애니메이션을 시작할 시기를 결정할 수도 있습니다.

다음 단계

이 자습서에서는 Microsoft Visual Studio Express 2012 for Windows 8의 기본 제공 페이지 템플릿을 사용하여 여러 페이지로 된 앱을 빌드하는 방법과 페이지 간에 이동하고 데이터를 전달하는 방법을 알아보았습니다. 그다음으로는 스타일과 템플릿을 사용하여 앱을 Windows 팀 블로그 웹 사이트의 특징에 맞게 하는 방법도 알아보았습니다. 또한 테마 애니메이션 및 앱 바를 사용하여 앱을 Windows 스토어 앱의 특징에 맞게 만드는 방법도 익혔습니다. 마지막으로 앱이 항상 최상으로 표시되도록 다양한 레이아웃과 방향에 적응시키는 방법을 알아보았습니다.

이제 이 앱은 Windows 스토어에 제출할 준비가 거의 다 되었습니다. Windows 스토어에 앱을 제출하는 방법에 대한 자세한 내용은 다음을 참조하세요.

관련 항목

C++로 작성한 Windows 스토어 앱용 로드맵
Visual Studio 11을 사용하여 Windows 스토어 앱 개발







본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
22 SQLite on Visual Studio with NuGet and Easy Instructions file 졸리운_곰 2016.08.11 329
21 Using SQLite in a Native Visual C++ Application 졸리운_곰 2016.08.11 276
20 How to: Ionic in Visual Studio 졸리운_곰 2016.05.09 594
19 Get started with Ionic apps in Visual Studio : 비주얼 스투이오로 모바일 앱 생성 ionic 프레임워크 졸리운_곰 2016.05.09 329
18 Build cross-platform apps with Xamarin in Visual Studio 졸리운_곰 2015.08.16 319
17 Visual C++를 사용하여 플랫폼 간 앱 제작 file 졸리운_곰 2015.08.16 645
16 Hooking 이란? file 졸리운_곰 2015.06.16 525
15 윈도우즈 후킹 기초 file 졸리운_곰 2015.06.16 318
14 윈도우 메시지 후킹 file 졸리운_곰 2015.06.16 2284
13 C# Obfuscation 난독화하기 file 졸리운_곰 2015.05.31 686
12 [학습정리] 민대규님의 학습정리 프로그램 "대굴이의 학습정리" file 졸리운_곰 2015.05.11 445
11 [일정관리] 민대규님 제작 "대굴이의 일정관리" file 졸리운_곰 2015.05.11 1841
10 webdav c++ lib 라이브러러 file 졸리운_곰 2015.05.11 395
9 Use Visual Studio C++ 6.0 on Windows 8.1 64bit : 윈도우 8.1에서 비주얼 스투디오 6.0 사용하기 졸리운_곰 2015.05.09 632
8 Visual Studio 2013을 사용하여 앱 개발 file 졸리운_곰 2014.05.30 842
7 C++로 작성한 Windows 스토어 앱용 로드맵 file 졸리운_곰 2014.05.30 570
» 블로그 뷰어 앱 만들기(C++) file 졸리운_곰 2014.05.30 3956
5 C++ Windows 스토어 앱에서 파일 선택기 사용(자습서 4/4) file 졸리운_곰 2014.05.30 762
4 C++ Windows 스토어 앱에서 탐색 및 보기 추가(자습서 3/4) file 졸리운_곰 2014.05.30 594
3 C++ Windows 스토어 앱의 수명 주기 및 상태 관리(자습서 2/4) file 졸리운_곰 2014.05.30 496
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED