Автор: Яковлев Игорь Сергеевич Источник: RSDN Magazine #4-2005
Опубликовано: 30.12.2005 Исправлено: 16.03.2006 Версия текста: 1.0
От редакции
Данная статья является введением в программирование на WinAPI. Важно понимать, что она носит скорее информационный характер, чем служит примером кода для реальных приложений. Дело в том, что WinAPI создавался для языка С, и имеет целый ряд недостатков в применении, как-то: невысокая безопасность, большой объем ручного кодирования для решения простейших задач, С-стиль, плохо выглядящий в C++-приложениях. Практически любое средство разработки на языке C++ для Windows включает те или иные высокоуровневые C++-библиотеки (MFC, ATL/WTL для Visual C++, VCL для C++ Builder), значительно упрощающие разработку Windows-приложений, и делающие ее более безопасной.
Предисловие
Эта статья посвящена описанию программирования приложений на «чистом» Win32 API. Она написана в основном для начинающих программистов, пишущих программы на Visual C++ 6 с использованием библиотеки MFC, но я надеюсь, может пригодиться и более опытным людям.
First Blood
После создания нового проекта Win32 Application, в зависимости от выбранных опций, мастер генерирует стартовый код. Из этого кода программисту впоследствии, и придется писать программу. Создавая новый проект Win32 Application, выберите в окне мастера опцию An empty project и добавьте в раздел Source Files новый файл с расширением .cpp.
В этом файле добавьте функцию WinMain вида:
Вот и готова первая программа на WinAPI. Она выводит сообщение, после чего завершает свою работу. Обратите внимание на параметры функции WinMain :
HINSTANCE hInstance – дескриптор экземпляра приложения. Этот дескриптор содержит адрес начала кода программы в ее адресном пространстве. Дескриптор hInstance чаще всего требуется функциям, работающим с ресурсами программы.
HINSTANCE hPrevInstance – дескриптор предыдущего экземпляра приложения. Этот дескриптор остался от старых версий Windows — скорее всего, вам он никогда не пригодится.
LPSTR lpCmdLine – указатель на начало командной строки, введенной при запуске программы.
int nCmdShow – это значение содержит желаемый вид окна (например, свернутый или развернутый)
Значение, которое возвращается функцией WinMain (тип int ) – код завершения программы. Принято, что если программа завершила свое выполнение без ошибок, возвращается 0.
Функция WinMain – первая функция, которая выполнятся в программе (ее еще называют «точка входа» или «entry point»). С нее все начинается, и ею (желательно) все должно закончиться.
ПРИМЕЧАНИЕ
Функция WinMain – это первая функция, которую вы можете увидеть и заполнить кодом. На самом деле до этой функции выполняется достаточно много кода из библиотеки C++
You have a Message!
Программисты, незнакомые с программированием на WinAPI, спросят: «Что с этим делать?!», — или: «Где создавать CDialog?». В данном случае ответ прост – нигде! В нашем проекте нет класса CDialog или, предположим, CButton – ведь эта статья посвящена тому, как обойтись без них.
В Windows при каждом событии, произошедшем в системе, отсылается «сообщение Windows» («windows message»). Эти сообщения уведомляют программу о событиях в системе, а программа в свою очередь, может на них реагировать. Сообщения может отсылать не только Windows, но и сами приложения. Это является одним из способов организации связи между процессами в системе. Конечно, программа может отсылать сообщения и самой себе.
СОВЕТ
Сообщение можно отослать функцией SendMessage или ее асинхронным аналогом PostMessage.
Для приема сообщений в программе должен находиться «цикл сообщений» («message loop») который обычно выглядит так:
Функция GetMessage принимает следующие параметры:
LPMSG lpMsg – указатель на структуру сообщения, в которую GetMessage вернет результат.
HWND hWnd – описатель окна, от которого GetMessage примет сообщение ( NULL означает, что GetMessage принимает сообщения от всех окон, принадлежащих потоку).
UINT wMsgFilterMin – наименьший идентификатор сообщения, которое примет GetMessage.
UINT wMsgFilterMax – наибольший идентификатор сообщения, которое примет GetMessage (если в значениях параметров wMsgFilterMin и wMsgFilterMax передать 0, функция будет принимать ВСЕ сообщения).
Функция GetMessage не отдает управление программе, пока не придет какое-либо сообщение. Если пришедшее сообщение – WM_QUIT , функция GetMessage вернет 0 . Тогда цикл прервется, и программа завершит свою работу. При любом другом сообщении функция GetMessage возвращает значение больше нуля, и начинатся выполнение тела цикла. При ошибке GetMessage возвращает -1.
СОВЕТ
Сообщение WM_QUIT лучше посылать с помощью специальной функции PostQuitMessage(int iExitCode). Эта функция отошлет сообщение WM_QUIT, а в параметре wParam передаст код завершения программы, указанный в iExitCode.
Функция DispatchMessage должна вызвать «функцию обработки сообщений». В простейшем варианте она выглядит так:
При вызове этой функции ей передаются следующие параметры:
HWND hWnd – описатель окна, от которого пришло сообщение.
UINT message – идентификатор сообщения.
WPARAM wParam и LPARAM lParam – параметры сообщения.
Функция обработки сообщений не обязательно должна иметь имя WndProc . Таких функций в программе может быть несколько, но их прототипы обязательно должны выглядеть так:
При вызове этой функции DispatchMessage передает в параметре message идентификатор сообщения. По этому идентификатору производится выборка и выполняется какое-либо действие ( «реакция на сообщение» ).
В Windows существует очень много сообщений! Писать обработчики для всех сообщений – нереальная задача. Чтобы Windows сама обработала бесполезное для вас сообщение, необходимо вызвать функцию DefWindowProc :
Желательно передавать все необработанные сообщения этой функции, а результат ее выполнения возвращать при выходе из WndProc . Это очень важно, так как от обработки некоторых сообщений Windows ждет возврата конкретных результатов или действий.
Одной функцией обработки сообщений могут пользоваться несколько окон, но для одного окна может существовать только одна функция обработки сообщений! Как же система определяет, какой именно функцией обработки сообщения пользоваться для конкретного окна и где она находится?! За это отвечает «класс окна» («window class»).
CLASSные окна
При создании нового окна ему присваивается «Класс окна» (window class). Класс окна задает оконную функцию, используемую по умолчанию. Кроме этого, класс окна задает другие параметры окна, такие, как стиль, меню окна, цвет рабочей области и т.д. Разные классы окон могут указывать на одну и ту же функцию обработки сообщений. Для создания класса его необходимо зарегистрировать.
Итак, регистрация! За нее отвечает функция RegisterClass . В ее параметре необходимо передать указатель на структуру WNDCLASS . Обычно для заполнения структуры и вызова RegisterClass создают отдельную функцию. Но это — дело вкуса.
Вот простейший пример такой функции:
WNDPROC lpfnWndProc – адрес функции обработки сообщений.
HINSTANCE hInstance – уже знакомая переменная, описывающая экземпляр.
LPCTSTR lpszClassName – имя нового класса.
HICON hCursor – описатель курсора мыши.
HBRUSH hbrBackground – цвет рабочей области окна.
Функция RegisterClass возвращает уникальный «описатель класса окна» типа ATOM . Если при регистрации класса произошла ошибка, это значение будет равно нулю. Чтобы узнать, что произошло, можно вызвать функцию GetLastError() .
Существует также функция RegisterClassEx. Это аналог функции RegisterClass с возможностью присвоения окнам маленькой иконки. При работе с этой функцией необходимо пользоваться структурой WNDCLASSEX.
ПРИМЕЧАНИЕ
Если вы решились работать с GUI Windows вручную, то пользоваться нужно именно RegisterClassEx, поскольку приложение, не имеющее маленькой иконки, сегодня выглядит в Windows как минимум странно. – прим.ред.
СОВЕТ
Следите, чтобы имя вашего класса не совпадало с именами системных классов (например: button или edit).
ПРИМЕЧАНИЕ
Я описал не всю структуру. Все незаполненные поля, которых нет в примере, сейчас равны нулю. Об их значениях можно узнать из MSDN.
Сообщения от окон, созданных на базе класса, зарегистрированного описанной выше функцией RegMyWindowClass, будут обрабатываться функцией с именем WndProc. Чтобы функция WndProc поняла, от какого именно окна пришло сообщение, ей передается уникальный описатель окна HWND .
Our Windows
На вашем месте у меня возникло бы желание увидеть те самые пресловутые окна, из-за которых столько шума. Окно в Windows создается функцией CreateWindow . Вот ее прототип:
Как видите, у функции множество параметров:
LPCTSTR lpClassName – имя класса для создаваемого окна (это имя использовалось при регистрации класса).
LPCTSTR lpWindowName – имя окна.
DWORD dwStyle – стиль окна.
int x – позиция по горизонтали верхнего левого угла окна.
int y – позиция по вертикали.
int nWidth – ширина окна.
int nHeight – высота окна.
HWND hWndParent – используется для создания «дочернего окна» («child window»). Сюда передается описатель «родительского окна» («parent window»).
HMENU hMenu – описатель меню (если hMenu равно нулю, используется меню класса, указанного в lpClassName ).
HINSTANCE hInstance – экземпляр приложения.
LPVOID lpParam – указатель на пользовательский параметр окна. Этот указатель со всеми остальными параметрами функции CreateWindow будет занесен в структуру CREATESTRUCT . В сообщениях WM_CREATE или WM_NCCREATE параметр lParam будет содержать указатель на эту структуру.
Функция CreateWindow возвращает уникальный описатель окна HWND . Если функция вернула ноль, значит, во время создания окна произошла ошибка. Какая именно, можно узнать, вызвав функцию GetLastError .
ПРИМЕЧАНИЕ
Существует также функция CreateWindowEx, в которой дополнительно присутствует параметр dwExStyle. С его помощью можно создать окно с дополнительными стилями.
План полета
Итак, сейчас я упрощенно расскажу, что же произойдет, если щелкнуть по окну левой кнопкой мыши.
Пользователь нажимает левую кнопку мыши в то время когда курсор мыши находится над рабочей областью окна.
Windows помещает сообщение WM_LBUTTONDOWN в очередь потока.
Цикл обработки сообщения должен вынуть сообщение с помощью функции GetMessage и передать его на обработку функции DispatchMessage .
Функция DispatchMessage находит окно, которому предназначено сообщение и помещает сообщение в его очередь.
Функция окна обрабатывает сообщение WM_LBUTTONDOWN и возвращает результат.
Тело цикла заканчивается, и управление снова передается функции GetMessage для ожидания новых сообщений.
Итого
WinMain, регистрация класса, цикл сообщений, функция обработки сообщений, создание окна. Как все это связать?! Вот код, который объединяет все написанное выше в одну программу:
Вот, в принципе, и все! Это полноценное приложение на WinAPI.
Программа регистрирует класс, создает окно этого класса и обслуживает сообщение WM_LBUTTONUP (оно приходит по событию отпускания левой кнопки мыши), показывает окно, и после обработки сообщения снова возвращается в цикл сообщений, находящийся в WinMain .
Признаю, что данная статья и программа опускает очень много деталей! Многие вещи были не раскрыты (например, остальные переменные структуры WNDCLASS ). Все это сделано для того, чтобы максимально упростить статью и уменьшить код программы.
Использование интерфейсов API с помощью C++/WinRT Consume APIs with C++/WinRT
В этом разделе показано, как использовать API-интерфейсы C++/WinRT, если они являются компонентом Windows, реализованы сторонним поставщиком компонентов или вами самостоятельно. This topic shows how to consume C++/WinRT APIs, whether they’re part of Windows, implemented by a third-party component vendor, or implemented by yourself.
Примеры кода в этом разделе компактны и просты. Вы можете воспроизвести их, создав проект консольного приложения Windows (C++/WinRT) и скопировав и вставив в него код. So that the code examples in this topic are short, and easy for you to try out, you can reproduce them by creating a new Windows Console Application (C++/WinRT) project, and copy-pasting code. Но вы не можете использовать произвольные пользовательские (сторонние) типы среды выполнения Windows из такого неупакованного приложения. However, you can’t consume arbitrary custom (third-party) Windows Runtime types from an unpackaged app like that. Оно позволяет использовать только типы Windows. You can consume only Windows types that way.
Чтобы использовать пользовательские (сторонние) типы среды выполнения Windows из консольного приложения, ему необходимо предоставить идентификатор пакета, чтобы оно могло разрешить регистрацию пользовательских типов. To consume custom (third-party) Windows Runtime types from a console app, you’ll need to give the app a package identity so that it can resolve the consumed custom types’ registration. Дополнительные сведения см. в статье о проект упаковки приложений Windows. For more info, see Windows Application Packaging Project.
Вы также можете создать проект из шаблонов для пустого приложения (C++/WinRT) , приложения основных компонентов (C++/WinRT) или компонента среды выполнения Windows (C++/WinRT) . Alternatively, create a new project from the Blank App (C++/WinRT), Core App (C++/WinRT), or Windows Runtime Component (C++/WinRT) project templates. Приложения таких типов уже включают идентификатор пакета. Those app types already have a package identity.
Если API находится в пространстве имен Windows If the API is in a Windows namespace
Это наиболее распространенный случай, в котором вы будете использовать API среды выполнения Windows. This is the most common case in which you’ll consume a Windows Runtime API. Для каждого типа в пространстве имен Windows, указанного в метаданных, C++/WinRT определяет подходящий для C++ эквивалент (называемый проецируемый тип). For every type in a Windows namespace defined in metadata, C++/WinRT defines a C++-friendly equivalent (called the projected type). Проецируемый тип имеет то же полное доменное имя, что и тип в Windows, но он размещается в пространстве имен C++ winrt с использованием синтаксиса C++. A projected type has the same fully-qualified name as the Windows type, but it’s placed in the C++ winrt namespace using C++ syntax. Например, Windows::Foundation:: URI проецируется в C++/WinRT как winrt::Windows::Foundation::Uri. For example, Windows::Foundation::Uri is projected into C++/WinRT as winrt::Windows::Foundation::Uri.
Приведем простой пример кода. Here’s a simple code example. Если вы хотите скопировать и вставить этот пример непосредственно в главный файл исходного кода проекта консольного приложения Windows (C++/WinRT) , сначала задайте параметр Не использовать предварительно скомпилированные заголовки в свойствах проекта. If you want to copy-paste the following code examples directly into the main source code file of a Windows Console Application (C++/WinRT) project, then first set Not Using Precompiled Headers in project properties.
Включенный заголовок winrt/Windows.Foundation.h входит в состав пакета SDK, который находится в папке %WindowsSdkDir%Include \cppwinrt\winrt\ . The included header winrt/Windows.Foundation.h is part of the SDK, found inside the folder %WindowsSdkDir%Include \cppwinrt\winrt\ . Заголовки в этой папке содержат типы пространства имен Windows, проецируемые в C++/WinRT. The headers in that folder contain Windows namespace types projected into C++/WinRT. В этом примере winrt/Windows.Foundation.h содержит winrt::Windows::Foundation::Uri, который представляет собой проецируемый тип для класса среды выполнения Windows::Foundation::Uri. In this example, winrt/Windows.Foundation.h contains winrt::Windows::Foundation::Uri, which is the projected type for the runtime class Windows::Foundation::Uri.
Каждый раз, когда вы хотите использовать тип из пространства имен Windows, включайте заголовок C++/WinRT, соответствующий этому пространству имен. Whenever you want to use a type from a Windows namespace, include the C++/WinRT header corresponding to that namespace. Директивы using namespace являются необязательными, но удобными. The using namespace directives are optional, but convenient.
В примере кода выше после инициализации C++/WinRT значение проецируемого типа winrt::Windows::Foundation::Uri помещается в стек с помощью одного из его задокументированных конструкторов (в этом примере — Uri(String)). In the code example above, after initializing C++/WinRT, we stack-allocate a value of the winrt::Windows::Foundation::Uri projected type via one of its publicly documented constructors (Uri(String), in this example). Для данного наиболее распространенного случая использования обычно это все, что нужно сделать. For this, the most common use case, that’s typically all you have to do. Когда у вас есть значение проецируемого типа C++/WinRT, вы можете работать с ним, как будто это экземпляр фактического типа среды выполнения Windows, поскольку он имеет те же члены. Once you have a C++/WinRT projected type value, you can treat it as if it were an instance of the actual Windows Runtime type, since it has all the same members.
На самом деле это проецируемое значение является своего рода псевдонимом; по сути это просто смарт-указатель на расположенный за ним объект. In fact, that projected value is a proxy; it’s essentially just a smart pointer to a backing object. Конструктор(ы) проецируемого значения вызывает RoActivateInstance для создания экземпляра опорного класса среды выполнения Windows (в этом случае — Windows.Foundation.Uri) и сохранения интерфейса по умолчанию этого объекта внутри нового проецируемого значения. The projected value’s constructor(s) call RoActivateInstance to create an instance of the backing Windows Runtime class (Windows.Foundation.Uri, in this case), and store that object’s default interface inside the new projected value. Как показано ниже, ваши вызовы членов проецируемого значения фактически делегируются, с помощью смарт-указателя, на стоящий за ним объект, и в нем и происходят изменения состояния. As illustrated below, your calls to the projected value’s members actually delegate, via the smart pointer, to the backing object; which is where state changes occur.
Когда значение contosoUri выходит за пределы области, оно разрушается и убирает ссылку на интерфейс по умолчанию. When the contosoUri value falls out of scope, it destructs, and releases its reference to the default interface. Если эта ссылка является последней ссылкой на опорный объект Windows.Foundation.Uri среды выполнения Windows, этот опорный объект также разрушается. If that reference is the last reference to the backing Windows Runtime Windows.Foundation.Uri object, the backing object destructs as well.
Проецируемый тип является оболочкой класса среды выполнения для использования его API-интерфейсов. A projected type is a wrapper over a runtime class for purposes of consuming its APIs. Проецируемый интерфейс является оболочкой интерфейса среды выполнения Windows. A projected interface is a wrapper over a Windows Runtime interface.
Для использования API-интерфейсов пространства имен Windows из C++/WinRT требуется включить заголовки из папки %WindowsSdkDir%Include \cppwinrt\winrt . To consume Windows namespace APIs from C++/WinRT, you include headers from the %WindowsSdkDir%Include \cppwinrt\winrt folder. Ситуация, когда тип в подчиненном пространстве имен ссылается на типы в его ближайшем родительском пространстве имен, является типичной. It’s common for a type in a subordinate namespace to reference types in its immediate parent namespace. Соответственно, каждый проецируемый заголовок C++/WinRT автоматически включает файл заголовков его родительского пространства имен, и поэтому вам не требуется явным образом включать его. Consequently, each C++/WinRT projection header automatically includes its parent namespace header file; so you don’t need to explicitly include it. Хотя если вы это сделаете, ошибки не будет. Although, if you do, there will be no error.
Например, для пространства имен Windows::Security::Cryptography::Certificates эквивалентные определения типов C++/WinRT располагаются в winrt/Windows.Security.Cryptography.Certificates.h . For example, for the Windows::Security::Cryptography::Certificates namespace, the equivalent C++/WinRT type definitions reside in winrt/Windows.Security.Cryptography.Certificates.h . Типам в Windows::Security::Cryptography::Certificates требуются типы в родительском пространстве имен Windows::Security::Cryptography, а типам в этом пространстве имен могут потребоваться типы в своем собственном родительском пространстве имен Windows::Security. Types in Windows::Security::Cryptography::Certificates require types in the parent Windows::Security::Cryptography namespace; and types in that namespace could require types in its own parent, Windows::Security.
Таким образом, при включении winrt/Windows.Security.Cryptography.Certificates.h этот файл в свою очередь содержит winrt/Windows.Security.Cryptography.h , и winrt/Windows.Security.Cryptography.h содержит в себе winrt/Windows.Security.h . So, when you include winrt/Windows.Security.Cryptography.Certificates.h , that file in turn includes winrt/Windows.Security.Cryptography.h ; and winrt/Windows.Security.Cryptography.h includes winrt/Windows.Security.h . И здесь цепочка прекращается, поскольку не содержит winrt/Windows.h . That’s where the trail stops, since there is no winrt/Windows.h . Этот процесс транзитивного включения останавливается на пространстве имен второго уровня. This transitive inclusion process stops at the second-level namespace.
Этот процесс транзитивно включает файлы заголовков, которые предоставляют необходимые объявления и реализации для классов, определенных в родительских пространствах имен. This process transitively includes the header files that provide the necessary declarations and implementations for the classes defined in parent namespaces.
Член типа в одном пространстве имен может ссылаться на один или несколько типов в другом несвязанном пространстве имен. A member of a type in one namespace can reference one or more types in other, unrelated, namespaces. Для того чтобы компилятор выполнил успешную компиляцию этих определений членов, компилятору нужно видеть объявления типов для закрытия всех этих типов. In order for the compiler to compile these member definitions successfully, the compiler needs to see the type declarations for the closure of all these types. Соответственно, каждый проецируемый заголовок C++/WinRT включает заголовки пространства имен, которые ему требуются для объявления всех зависимых типов. Consequently, each C++/WinRT projection header includes the namespace headers it needs to declare any dependent types. В отличие от родительских пространств имен в этом процессе не перенимаются реализации для ссылочных типов. Unlike for parent namespaces, this process does not pull in the implementations for referenced types.
Если вы хотите действительно использовать тип (создать экземпляр, вызывать методы и т. п.), объявленный в несвязанном пространстве имен, вам необходимо включить файл заголовков нужного пространства имен для этого типа. When you want to actually use a type (instantiate, call methods, etc.) declared in an unrelated namespace, you must include the appropriate namespace header file for that type. Автоматически включаются только объявления, но не реализации. Only declarations, not implementations, are automatically included.
Например, если вы только включите winrt/Windows.Security.Cryptography.Certificates.h , то это приведет к получению объявлений из этих пространств имен (и далее транзитивным образом). For example, if you only include winrt/Windows.Security.Cryptography.Certificates.h , then that causes declarations to be pulled in from these namespaces (and so on, transitively).
Другими словами, некоторые API-интерфейсы объявляются в заголовке, который вы включили. In other words, some APIs are forward-declared in a header that you’ve included. Но их определения находятся в заголовке, который вы еще не включили. But their definitions are in a header that you haven’t yet included. Поэтому, если затем вызвать Windows::Foundation::Uri::RawUri, то вы получите ошибку компоновщика, указывающую, что элемент не определен. So, if you then call Windows::Foundation::Uri::RawUri, then you’ll receive a linker error indicating that the member is undefined. Решением является явное #include . The solution is to explicitly #include . Как правило при отображении ошибки компоновщика, такой как эта, включите заголовок с именем для пространства имен API и повторите сборку. In general, when you see a linker error such as this, include the header named for the API’s namespace, and rebuild.
Доступ к членам через объект, через интерфейс или с помощью ABI Accessing members via the object, via an interface, or via the ABI
С помощью проекции C++/WinRT представление времени выполнения для класса среды выполнения Windows представляет собой не что иное, как лежащие в основе базовые интерфейсы ABI. With the C++/WinRT projection, the runtime representation of a Windows Runtime class is no more than the underlying ABI interfaces. Но для удобства можно использовать классы в коде так, как задумывалось их автором. But, for your convenience, you can code against classes in the way that their author intended. Например, можно вызвать метод ToString для Uri, как если бы он был методом класса (на самом деле, по своей сути это метод отдельного интерфейса IStringable). For example, you can call the ToString method of a Uri as if that were a method of the class (in fact, under the covers, it’s a method on the separate IStringable interface).
WINRT_ASSERT — это макроопределение, которое передается в _ASSERTE. WINRT_ASSERT is a macro definition, and it expands to _ASSERTE.
Такое удобство обеспечивается путем запроса к соответствующему интерфейсу. This convenience is achieved via a query for the appropriate interface. Но вы всегда сохраняете контроль. But you’re always in control. Вы можете пожертвовать частью этого удобства в пользу повышения производительности путем самостоятельного извлечения интерфейса IStringable и его непосредственного использования. You can opt to give away a little of that convenience for a little performance by retrieving the IStringable interface yourself and using it directly. В следующем примере кода вы получите фактический указатель на интерфейс IStringable во время выполнения (с помощью одноразового запроса). In the code example below, you obtain an actual IStringable interface pointer at run time (via a one-time query). После этого ваш вызов ToString будет прямым и позволит избежать любых дальнейших обращений к QueryInterface. After that, your call to ToString is direct, and avoids any further call to QueryInterface.
Можно выбрать этот метод, если вы знаете, что будете вызывать несколько методов одного интерфейса. You might choose this technique if you know you’ll be calling several methods on the same interface.
Кстати, если вам потребуется доступ к членам на уровне ABI, его можно получить. Incidentally, if you do want to access members at the ABI level then you can. В примере кода ниже показано, как это сделать, а дополнительные сведения и примеры кода можно найти в разделе Взаимодействие между C++/WinRT и ABI. The code example below shows how, and there are more details and code examples in Interop between C++/WinRT and the ABI.
Отложенная инициализация Delayed initialization
В C++/WinRT каждый тип проекции имеет специальный конструктор C++/WinRT std::nullptr_t. In C++/WinRT, each projected type has a special C++/WinRT std::nullptr_t constructor. Наряду с исключением все конструкторы типа проекции, включая конструктор по умолчанию, создают базовый объект среды выполнения Windows и предоставляют смарт-указатель на него. With the exception of that one, all projected-type constructors—including the default constructor—cause a backing Windows Runtime object to be created, and give you a smart pointer to it. Таким образом, это правило применяется везде, где используется конструктор по умолчанию, например для неинициализированных локальных переменных, неинициализированных глобальных переменных и неинициализированных переменных-членов. So, that rule applies anywhere that the default constructor is used, such as uninitialized local variables, uninitialized global variables, and uninitialized member variables.
Вы можете создать переменную типа проекции без необходимости создавать базовый объект среды выполнения Windows (чтобы сделать это позже). If, on the other hand, you want to construct a variable of a projected type without it in turn constructing a backing Windows Runtime object (so that you can delay that work until later), then you can do that. Объявите переменную или поле с помощью специального конструктора C++/WinRT std::nullptr_t (который внедряет проекцию C++/WinRT в каждый класс среды выполнения). Declare your variable or field using that special C++/WinRT std::nullptr_t constructor (which the C++/WinRT projection injects into every runtime class). Этот специальный конструктор используется с m_gamerPicBuffer в приведенном ниже примере кода. We use that special constructor with m_gamerPicBuffer in the code example below.
При использовании конструкторов типа проекции, кроме конструктора std::nullptr_t, создается базовый объект среды выполнения Windows. All constructors on the projected type except the std::nullptr_t constructor cause a backing Windows Runtime object to be created. Конструктор std::nullptr_t по сути является безоперационным. The std::nullptr_t constructor is essentially a no-op. Он ожидает инициализацию проецируемого объекта в более поздний момент. It expects the projected object to be initialized at a subsequent time. Таким образом, независимо от того, имеет ли класс среды выполнения конструктор или нет, этот способ можно использовать для эффективной отложенной инициализации. So, whether a runtime class has a default constructor or not, you can use this technique for efficient delayed initialization.
Это соображение влияет на другие места, где вы вызываете конструктор по умолчанию, например, на векторы и карты. This consideration affects other places where you’re invoking the default constructor, such as in vectors and maps. Рассмотрим пример кода, для которого вам потребуется проект Пустое приложение (C++/WinRT) . Consider this code example, for which you’ll need a Blank App (C++/WinRT) project.
Присвоение создает новый TextBlock, а затем немедленно перезаписывает его value . The assignment creates a new TextBlock, and then immediately overwrites it with value . Вот средство. Here’s the remedy.
Не допускайте ошибку, выполняя инициализацию с задержкой Don’t delay-initialize by mistake
Следите за тем, чтобы не вызвать конструктор std::nullptr_t по ошибке. Be careful that you don’t invoke the std::nullptr_t constructor by mistake. При разрешении конфликтов компилятор будет отдавать предпочтение ему, а не конструкторам фабрики. The compiler’s conflict resolution favors it over the factory constructors. Например, рассмотрим такие два определения классов среды выполнения. For example, consider these two runtime class definitions.
Предположим, мы хотим создать неизолированный объект Gift (объект Gift, созданный с неинициализированным типом GiftBox). Let’s say that we want to construct a Gift that isn’t inside a box (a Gift that’s constructed with an uninitialized GiftBox). Сначала рассмотрим, как не нужно это делать. First, let’s look at the wrong way to do that. Мы знаем, что существует конструктор Gift, который принимает параметр GiftBox. We know that there’a Gift constructor that takes a GiftBox. Но если мы захотим передать параметру GiftBox значение NULL (вызвав конструктор Gift с помощью унифицированной инициализации, как мы делаем ниже), мы не получим нужный результат. But if we’re tempted to pass a null GiftBox (invoking the Gift constructor via uniform initialization, as we do below), then we won’t get the result we want.
Мы получим неинициализированный объект Gift, What you get here is an uninitialized Gift. а не объект Gift с неинициализированным параметром GiftBox. You don’t get a Gift with an uninitialized GiftBox. Вот как правильно это сделать. Here’s the correct way to do that.
В примере с неправильными действиями при передаче литерала nullptr конфликт разрешается в пользу конструктора с задержкой инициализации. In the incorrect example, passing a nullptr literal resolves in favor of the delay-initializing constructor. Чтобы конфликт разрешился в пользу конструктора фабрики, параметр должен иметь тип GiftBox. To resolve in favor of the factory constructor, the type of the parameter must be a GiftBox. При этом вы все равно можете передать явный тип GiftBox с задержкой инициализации, как показано в примере с правильными действиями. You still have the option to pass an explicitly delay-initializing GiftBox, as shown in the correct example.
В следующем примере также приведены правильные действия, так как параметр имеет тип GiftBox, а не std::nullptr_t. This next example is also correct, because the parameter has type GiftBox, and not std::nullptr_t.
Путаница возникает только при передаче литерала nullptr . It’s only when you pass a nullptr literal that the ambiguity arises.
Не допускайте ошибку, вызывая конструктор с копированием Don’t copy-construct by mistake.
Это предупреждение аналогично описанному выше предупреждению в разделе Не допускайте ошибку, выполняя инициализацию с задержкой. This caution is similar to the one described in the Don’t delay-initialize by mistake section above.
Кроме конструктора с задержкой инициализации, проекция C++/WinRT также внедряет конструктор с копированием в каждый класс среды выполнения. In addition to the delay-initializing constructor, the C++/WinRT projection also injects a copy constructor into every runtime class. Это конструктор с одним параметром, который принимает тот же тип, который имеет создаваемый объект. It’s a single-parameter constructor that accepts the same type as the object being constructed. Полученный смарт-указатель указывает на тот же базовый объект среды выполнения Windows, на который указывает параметр конструктора. The resulting smart pointer points to the same backing Windows Runtime object as that pointed to by its constructor parameter. В результате создаются два объекта со смарт-указателями, которые указывают на один базовый объект. The result is two smart pointer objects pointing to the same backing object.
Ниже приведено определение класса среды выполнения, которое мы будем использовать в примерах с кодом. Here’s a runtime class definition that we’ll use in the code examples.
Предположим, мы хотим создать объект GiftBox в более крупном объекте GiftBox. Let’s say that we want to construct a GiftBox inside a larger GiftBox.
Правильным способом будет явно вызвать фабрику активации. The correct way to do it is to call the activation factory explicitly.
Если API-интерфейс реализован в компоненте среды выполнения Windows If the API is implemented in a Windows Runtime component
Этот раздел применим, если вы создали компонент самостоятельно или получили его от поставщика. This section applies whether you authored the component yourself, or it came from a vendor.
Сведения об установке и использовании расширения C++/WinRT для Visual Studio (VSIX) и пакета NuGet (которые вместе обеспечивают поддержку шаблона проекта и сборки) см. в разделе о поддержке C++/WinRT в Visual Studio. For info about installing and using the C++/WinRT Visual Studio Extension (VSIX) and the NuGet package (which together provide project template and build support), see Visual Studio support for C++/WinRT.
В своем проекте приложения обратитесь к файлу метаданных среды выполнения Windows компонента для компонента среды выполнения Windows ( .winmd ) и выполните сборку. In your application project, reference the Windows Runtime component’s Windows Runtime metadata ( .winmd ) file, and build. Во время сборки cppwinrt.exe средство создает стандартную C++ библиотеку, которая полностью описывает—или проекты—поверхность API для компонента. During the build, the cppwinrt.exe tool generates a standard C++ library that fully describes—or projects—the API surface for the component. Другими словами, созданная библиотека содержит проецируемые типы для компонента. In other words, the generated library contains the projected types for the component.
Затем, как и в случае типа пространства имен Windows, необходимо включить заголовок и создать проецируемый типа через один из его конструкторов. Then, just as for a Windows namespace type, you include a header and construct the projected type via one of its constructors. Код запуска проекта вашего приложения регистрирует класс среды выполнения, а конструктор проецируемого типа вызывает RoActivateInstance для активации класса среды выполнения из указанного компонента. Your application project’s startup code registers the runtime class, and the projected type’s constructor calls RoActivateInstance to activate the runtime class from the referenced component.
Дополнительные сведения, код и пошаговое руководство по потреблению API-интерфейсов, реализованных в компоненте среды выполнения Windows, см. в статьях Создание компонентов среды выполнения Windows с помощью C++/WinRT и Создание событий в C++/WinRT. For more details, code, and a walkthrough of consuming APIs implemented in a Windows Runtime component, see Windows Runtime components with C++/WinRT and Author events in C++/WinRT.
Если API-интерфейс реализован в использующем проекте If the API is implemented in the consuming project
Пример кода в этом разделе приведен из раздела Элементы управления XAML; привязка к свойству C++/WinRT. The code example in this section is taken from the topic XAML controls; bind to a C++/WinRT property. В этом разделе вы также найдете дополнительные сведения, код и пошаговое руководство по использованию класса среды выполнения, реализованного в использующем его проекте. See that topic for more details, code, and a walkthrough of consuming a runtime class that’s implemented in the same project that consumes it.
Тип, используемый из пользовательского интерфейса XAML, должен являться классом среды выполнения, даже если он находится в том же проекте, что и XAML. A type that’s consumed from XAML UI must be a runtime class, even if it’s in the same project as the XAML. В этом сценарии создается проецируемый тип из метаданных среды выполнения Windows класса среды выполнения ( .winmd ). For this scenario, you generate a projected type from the runtime class’s Windows Runtime metadata ( .winmd ). Опять же, вы включаете заголовок, но можете выбрать между способами создания экземпляра класса среды выполнения, доступными в версии C++/WinRT 1.0 и версии 2.0. Again, you include a header, but then you have a choice between the C++/WinRT version 1.0 or version 2.0 ways of constructing the instance of the runtime class. Метод версии 1.0 использует winrt::make; метод версии 2.0 также называется универсальным созданием. The version 1.0 method uses winrt::make; the version 2.0 method is known as uniform construction. Давайте рассмотрим каждый из них. Let’s look at each in turn.
Создание с помощью winrt::make Constructing by using winrt::make
Начнем с метода по умолчанию (C++/WinRT версии 1.0), так как это позволит ознакомиться с таким подходом. Let’s start with the default (C++/WinRT version 1.0) method, because it’s a good idea to be at least familiar with that pattern. Нам нужно создать проецируемый тип с помощью конструктора std::nullptr_t. You construct the projected type via its std::nullptr_t constructor. Этот конструктор не выполняет инициализацию, поэтому далее необходимо назначить значение экземпляру через вспомогательную функцию winrt::make, передав все необходимые аргументы конструктора. That constructor doesn’t perform any initialization, so you must next assign a value to the instance via the winrt::make helper function, passing any necessary constructor arguments. Класс среды выполнения, реализованный в том же проекте, что и использующий его код, не обязательно регистрировать, а также необязательно создавать его экземпляр посредством активации среды выполнения Windows/COM. A runtime class implemented in the same project as the consuming code doesn’t need to be registered, nor instantiated via Windows Runtime/COM activation.
Полное пошаговое руководство см. в статье Элементы управления XAML; привязка к свойству C++/WinRT. See XAML controls; bind to a C++/WinRT property for a full walkthrough. В этом разделе приведены выдержки из этого руководства. This section shows extracts from that walkthrough.
Универсальное создание Uniform construction
Начиная с версии C++/WinRT 2.0 вам доступна оптимизированная форма создания, известная как универсальное создание (см. раздел Новости и изменения в C++/WinRT 2.0). With C++/WinRT version 2.0 and later, there’s an optimized form of construction available to you known as uniform construction (see News, and changes, in C++/WinRT 2.0).
Полное пошаговое руководство см. в статье Элементы управления XAML; привязка к свойству C++/WinRT. See XAML controls; bind to a C++/WinRT property for a full walkthrough. В этом разделе приведены выдержки из этого руководства. This section shows extracts from that walkthrough.
Чтобы использовать универсальное создание, а не winrt::make, вам потребуется фабрика активации. To use uniform construction instead of winrt::make, you’ll need an activation factory. Рекомендуемым методом ее создания является добавление конструктора к IDL-файлу. A good way to generate one is to add a constructor to your IDL.
Затем в MainPage.h объявите и инициализируйте m_mainViewModel всего за один шаг, как показано ниже. Then, in MainPage.h declare and initialize m_mainViewModel in just one step, as shown below.
В конструкторе MainPage в файле MainPage.cpp не требуется указывать код m_mainViewModel = winrt::make (); . And then, in the MainPage constructor in MainPage.cpp , there’s no need for the code m_mainViewModel = winrt::make (); .
Дополнительные сведения об универсальном создании и примеры кода см. в статье о предоставлении согласия на использование универсального создания и прямого обращения к реализации. For more info about uniform construction, and code examples, see Opt in to uniform construction, and direct implementation access.
Создание и возврат прогнозируемых типов и интерфейсов Instantiating and returning projected types and interfaces
Вот пример того, как проецируемые типы и интерфейсы могут выглядеть в вашем использующем их проекте. Here’s an example of what projected types and interfaces might look like in your consuming project. Помните, что проецируемых типы (например, указанному в этом примере), формируемые инструментами и не будут созданы самостоятельно. Remember that a projected type (such as the one in this example), is tool-generated, and is not something that you’d author yourself.
MyRuntimeClass является проецируемым типом; проецируемые интерфейсы включают IMyRuntimeClass, IStringable и IClosable. MyRuntimeClass is a projected type; projected interfaces include IMyRuntimeClass, IStringable, and IClosable. В этом разделе были продемонстрированы различные способы создания экземпляра проецируемого типа. This topic has shown the different ways in which you can instantiate a projected type. Вот напоминание и краткая сводка, использующая в качестве примера MyRuntimeClass. Here’s a reminder and summary, using MyRuntimeClass as an example.
Можно осуществлять доступ к членам всех интерфейсов проецируемого типа. You can access the members of all of the interfaces of a projected type.
Можно возвращать проецируемый тип вызывающему объекту. You can return a projected type to a caller.
Проецируемые типы и интерфейсы являются производными от winrt::Windows::Foundation::IUnknown. Projected types and interfaces derive from winrt::Windows::Foundation::IUnknown. Поэтому можно вызывать IUnknown::as для проецируемого типа или интерфейса для запроса других проецируемых интерфейсов, которые также можно использовать или вернуть вызывающему объекту. So, you can call IUnknown::as on a projected type or interface to query for other projected interfaces, which you can also either use or return to a caller. Функция-член as работает как QueryInterface. The as member function works like QueryInterface.
Фабрики активации Activation factories
Удобный прямолинейный способ создания объекта C++/WinRT выглядит следующим образом. The convenient, direct way to create a C++/WinRT object is as follows.
Но бывают ситуации, в которых вам потребуется создать фабрику активации самостоятельно, а затем создавать объекты из нее по мере необходимости. But there may be times that you’ll want to create the activation factory yourself, and then create objects from it at your convenience. Вот несколько примеров, показывающих использование шаблона функции winrt::get_activation_factory. Here are some examples showing you how, using the winrt::get_activation_factory function template.
Классы в двух приведенных выше примерах относятся к типам из пространства имен Windows. The classes in the two examples above are types from a Windows namespace. В следующем примере ThermometerWRC::Thermometer — это пользовательский тип, реализованный в компоненте среды выполнения Windows. In this next example, ThermometerWRC::Thermometer is a custom type implemented in a Windows Runtime component.
Неоднозначность членов и типов Member/Type ambiguities
Если функция-член имеет то же имя, что и тип, это является неоднозначностью. When a member function has the same name as a type, there’s ambiguity. В C++ правила поиска неквалифицированного имени в функциях-членах таковы, что поиск класса выполняется перед поиском в пространствах имен. The rules for C++ unqualified name lookup in member functions cause it to search the class before searching in namespaces. Правило неудавшаяся подстановка — не ошибка (SFINAE) не применяется (оно применяется во время разрешения перегрузки шаблонов функций). The substitution failure is not an error (SFINAE) rule doesn’t apply (it applies during overload resolution of function templates). Поэтому, если не удается выполнить подстановку имени, находящегося внутри класса, компилятор не ищет лучшее соответствие, а просто сообщает об ошибке. So if the name inside the class doesn’t make sense, then the compiler doesn’t keep looking for a better match—it simply reports an error.
В примере выше компилятор считает, что вы передаете FrameworkElement.Style() (который в C++/WinRT является функцией-членом) в качестве параметра шаблона в IUnknown::as. Above, the compiler thinks that you’re passing FrameworkElement.Style() (which, in C++/WinRT, is a member function) as the template parameter to IUnknown::as. Решение заключается в том, чтобы принудительно интерпретировать имя Style как тип Windows::UI::Xaml::Style. The solution is to force the name Style to be interpreted as the type Windows::UI::Xaml::Style.
Правило поиска неквалифицированного имени имеет специальное исключение, если за именем следует :: . В этом случае функции, переменные и значения перечисления игнорируются. Unqualified name lookup has a special exception in the case that the name is followed by :: , in which case it ignores functions, variables, and enum values. Это позволяет выполнять такие действия. This allows you to do things like this.