Обработка событий с помощью делегатов в C++/WinRT

Это важно

Разрабатываете с помощью Windows App SDK? В коде этой статьи используются пространства имен UWP (Windows.UI.Xaml). Если ваш проект ориентирован на WinUI 3 (Windows App SDK), замените Microsoft.UI.Xaml (и связанные пространства имен Microsoft.UI.*) повсюду. Дополнительные сведения см. в руководстве по сопоставлению API UWP с Windows App SDK для полного сопоставления и миграции пользовательского интерфейса.

В этом разделе показано, как зарегистрировать и отозвать делегаты обработки событий с помощью C++/WinRT. Вы можете обрабатывать событие с помощью любого стандартного объекта функции C++.

Note

Сведения об установке и использовании расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки), см. Visual Studio поддержку C++/WinRT.

Добавление обработчика событий с помощью Visual Studio

Удобный способ добавления обработчика событий в проект — использование пользовательского интерфейса конструктора XAML в Visual Studio. Откройте страницу XAML в конструкторе XAML, выберите элемент управления, событие которого требуется обрабатывать. На странице свойств для этого элемента управления щелкните значок молнии, чтобы отобразить список всех событий, связанных с этим элементом управления. Затем дважды щелкните событие, которое требуется обрабатывать; например, OnClicked.

Конструктор XAML добавляет в исходные файлы соответствующий прототип функции обработчика событий (и реализацию-заглушку), чтобы вы могли заменить её собственной реализацией.

Note

Как правило, обработчики событий не должны быть описаны в файле Midl (.idl). Поэтому конструктор XAML не добавляет прототипы функций обработчика событий в файл Midl. Он лишь добавляет их в ваши файлы .h и .cpp.

Регистрация делегата для обработки события

Простой пример — обработка события нажатия кнопки. Обычно для регистрации функции-члена, обрабатывающей событие, используется разметка XAML, например, так.

// MainPage.xaml
<Button x:Name="myButton" Click="ClickHandler">Click Me</Button>
// MainPage.h
void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */)
{
    myButton().Content(box_value(L"Clicked"));
}

Приведенный выше код взят из проекта "Пустое приложение" (WinUI 3 в классическом приложении) в Visual Studio. Код myButton() вызывает созданную функцию доступа, которая возвращает кнопку , которую мы назвали myButton. Если изменить x:Name этого элемента Button, то имя сгенерированной функции доступа тоже изменится.

Note

В этом случае источник событий (объект, который вызывает событие), является кнопкойс именем myButton. И получатель события (объект, обрабатывающий событие), является экземпляром MainPage. Дополнительные сведения см. в этом разделе об управлении временем существования источников событий и получателей событий.

Вместо декларативного действия в разметке можно принудительно зарегистрировать функцию-член для обработки события. Возможно, это не очевидно из приведенного ниже примера кода, но аргумент для вызова ButtonBase::Click является экземпляром делегата RoutedEventHandler . В этом случае мы используем перегрузку конструктора RoutedEventHandler, которая принимает объект и указатель на функцию-член.

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click({ this, &MainPage::ClickHandler });
}

Это важно

При регистрации делегата приведенный выше пример кода передает необработанный этот указатель (указывающий на текущий объект). Чтобы узнать, как установить сильную или слабую ссылку на текущий объект, см. статью "Если вы используете функцию-член в качестве делегата".

Ниже приведен пример, использующий статическую функцию-член; обратите внимание на более простой синтаксис.

// MainPage.h
static void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click( MainPage::ClickHandler );
}
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */) { ... }

Существуют другие способы создания RoutedEventHandler. Ниже приведен блок синтаксиса, взятый из раздела документации для RoutedEventHandler (выберите C++/WinRT в раскрывающемся списке "Язык " в правом верхнем углу веб-страницы). Обратите внимание на различные конструкторы: один принимает лямбда-выражение; другой — свободную функцию; а ещё один (тот, который мы использовали выше) принимает объект и указатель на функцию-член.

struct RoutedEventHandler : winrt::Windows::Foundation::IUnknown
{
    RoutedEventHandler(std::nullptr_t = nullptr) noexcept;
    template <typename L> RoutedEventHandler(L lambda);
    template <typename F> RoutedEventHandler(F* function);
    template <typename O, typename M> RoutedEventHandler(O* object, M method);
    /* ... other constructors ... */
    void operator()(winrt::Windows::Foundation::IInspectable const& sender,
        winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e) const;
};

Также полезно рассмотреть синтаксис оператора вызова функции. Здесь указано, какими должны быть параметры делегата. Как видно, в этом случае синтаксис оператора вызова функции соответствует параметрам нашего MainPage::ClickHandler.

Note

Чтобы узнать сведения о делегате любого события и его параметрах, сначала перейдите в раздел документации, посвящённый самому событию. Рассмотрим событие UIElement.KeyDown в качестве примера. Посетите этот раздел и выберите C++/WinRT в раскрывающемся списке "Язык ". В блоке синтаксиса в начале раздела вы увидите это.

// Register
event_token KeyDown(KeyEventHandler const& handler) const;

Эта информация показывает, что событие UIElement.KeyDown (о котором идет речь) использует тип делегата KeyEventHandler, поскольку именно этот тип передается при регистрации делегата для события данного типа. Итак, теперь перейдите по ссылке по этой теме к типу делегата KeyEventHandler. Здесь блок синтаксиса содержит оператор вызова функции. И, как упоминалось выше, это показывает, какими должны быть параметры вашего делегата.

void operator()(
  winrt::Windows::Foundation::IInspectable const& sender,
  winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

Как видите, делегат должен быть объявлен так, чтобы принимать IInspectable в качестве sender и экземпляр класса KeyRoutedEventArgs в качестве args.

Для примера рассмотрим событие Popup.Closed. Его тип делегата — EventHandler<IInspectable>. Таким образом, делегат будет принимать IInspectable в качестве отправителя, а другой IInspectable (так как это параметр типа EventHandler) в качестве args.

Если вы не выполняете много работы в обработчике событий, можно использовать лямбда-функцию вместо функции-члена. Опять же, это может быть не очевидно из приведенного ниже примера кода, но делегат RoutedEventHandler создается из лямбда-функции, которая, опять же, должна соответствовать синтаксису оператора вызова функции, который мы обсуждали выше.

MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click([this](IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        myButton().Content(box_value(L"Clicked"));
    });
}

При создании делегата вы можете сделать это немного более явным. Например, если вы хотите передавать его другим или использовать его несколько раз.

MainPage::MainPage()
{
    InitializeComponent();

    auto click_handler = [](IInspectable const& sender, RoutedEventArgs const& /* args */)
    {
        sender.as<winrt::Microsoft::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

Отзыв зарегистрированного делегата

При регистрации делегата вам обычно возвращается токен. Впоследствии вы можете использовать этот токен, чтобы отменить регистрацию делегата; это означает, что делегат будет отписан от события и не будет вызван, если событие будет снова сгенерировано.

Для простоты ни один из приведенных выше примеров кода не показал, как это сделать. Но в следующем примере кода токен сохраняется в закрытом члене данных структуры, а его обработчик отзывается в деструкторе.

struct Example : ExampleT<Example>
{
    Example(winrt::Microsoft::UI::Xaml::Controls::Button const& button) : m_button(button)
    {
        m_token = m_button.Click([this](IInspectable const&, RoutedEventArgs const&)
        {
            // ...
        });
    }
    ~Example()
    {
        m_button.Click(m_token);
    }

private:
    winrt::Microsoft::UI::Xaml::Controls::Button m_button;
    winrt::event_token m_token;
};

Вместо строгой ссылки, как в приведенном выше примере, можно сохранить слабую ссылку на кнопку (см. статью "Сильные и слабые ссылки" в C++/WinRT).

Note

Когда источник событий генерирует свои события синхронно, вы можете отключить свой обработчик и быть уверены, что больше не получите никаких событий. Но для асинхронных событий даже после отзыва (и особенно при отмене в деструкторе) событие в полете может достичь объекта после начала деструкции. Если найти место для отмены подписки до уничтожения, это может смягчить проблему, а для надежного решения см. Безопасный доступ к указателю this в делегате обработки событий.

Кроме того, при регистрации делегата можно указать winrt::auto_revoke (это значение типа winrt::auto_revoke_t), чтобы запросить отзыв события (типа winrt::event_revoker). Отменяющий событие содержит слабую ссылку на источник событий (объект, который вызывает событие) для вас. Вы можете вручную отозвать регистрацию, вызвав функцию-член event_revoker::revoke; однако объект event revoker автоматически вызывает эту функцию, когда выходит из области видимости. Функция revoke проверяет, всё ли ещё существует источник события, и, если да, отменяет регистрацию вашего делегата. В этом примере нет необходимости хранить источник событий и не требуется деструктор.

struct Example : ExampleT<Example>
{
    Example(winrt::Microsoft::UI::Xaml::Controls::Button button)
    {
        m_event_revoker = button.Click(
            winrt::auto_revoke,
            [this](IInspectable const& /* sender */,
            RoutedEventArgs const& /* args */)
        {
            // ...
        });
    }

private:
    winrt::Microsoft::UI::Xaml::Controls::Button::Click_revoker m_event_revoker;
};

Ниже приведен блок синтаксиса, взятый из раздела документации для события ButtonBase::Click . В нем показаны три различных функции регистрации и отзыва. Вы можете точно увидеть, какой тип отзыва событий необходимо объявить из третьей перегрузки. И вы можете передавать одни и те же типы делегатов в обе перегрузки: register и revoke with event_revoker.

// Register
winrt::event_token Click(winrt::Microsoft::UI::Xaml::RoutedEventHandler const& handler) const;

// Revoke with event_token
void Click(winrt::event_token const& token) const;

// Revoke with event_revoker
Button::Click_revoker Click(winrt::auto_revoke_t,
    winrt::Microsoft::UI::Xaml::RoutedEventHandler const& handler) const;

Note

В приведённом выше примере кода Button::Click_revoker — это псевдоним типа для winrt::event_revoker<winrt::Microsoft::UI::Xaml::Controls::Primitives::IButtonBase>. Аналогичный шаблон применяется ко всем событиям C++/WinRT. Для каждого события среда выполнения Windows существует перегрузка функции отзыва регистрации, которая возвращает объект отзыва регистрации события, а тип этого объекта является членом источника события. Таким образом, для получения другого примера событие Window::SizeChanged имеет перегрузку функции регистрации, которая возвращает значение типа Window::SizeChanged_revoker.

Вы можете рассмотреть возможность отзыва обработчиков в сценарии навигации по страницам. Если вы неоднократно переходите на страницу, а затем покидаете её, то при уходе со страницы можно отменять регистрацию любых обработчиков. В качестве альтернативы, если вы повторно используете тот же экземпляр страницы, проверьте значение токена и выполняйте регистрацию, только если оно ещё не задано (if (!m_token){ ... }). Третий вариант — сохранить отзыв события на странице в качестве члена данных. И четвертый вариант, как описано далее в этом разделе, заключается в захвате сильной или слабой ссылки на этот объект в лямбда-функции.

Если делегат автоматического отзыва не удается зарегистрировать

Если при попытке указать winrt::auto_revoke при регистрации делегата возникает исключение winrt::hresult_no_interface, то обычно это означает, что источник событий не поддерживает слабые ссылки. Это распространенная ситуация, например, в пространстве имен Microsoft.UI.Composition. В этой ситуации вы не можете использовать функцию автоматического отзыва. Вам придется вернуться к ручному отзыву обработчиков событий.

Типы делегатов для асинхронных действий и операций

В приведенных выше примерах используется тип делегата RoutedEventHandler , но есть, конечно, много других типов делегатов. Например, асинхронные действия и операции (с ходом выполнения и без него) имеют события завершения и/или хода выполнения, для которых ожидаются делегаты соответствующего типа. Например, для события хода выполнения асинхронной операции с отслеживанием хода выполнения (то есть любого объекта, реализующего IAsyncOperationWithProgress) требуется делегат типа AsyncOperationProgressHandler. Ниже приведен пример кода для создания делегата этого типа с помощью лямбда-функции. В примере также показано, как создать делегат AsyncOperationWithProgressCompletedHandler .

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    auto async_op_with_progress = syndicationClient.RetrieveFeedAsync(rssFeedUri);

    async_op_with_progress.Progress(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& /* sender */,
            RetrievalProgress const& args)
        {
            uint32_t bytes_retrieved = args.BytesRetrieved;
            // use bytes_retrieved;
        });

    async_op_with_progress.Completed(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& sender,
            AsyncStatus const /* asyncStatus */)
        {
            SyndicationFeed syndicationFeed = sender.GetResults();
            // use syndicationFeed;
        });

    // or (but this function must then be a coroutine, and return IAsyncAction)
    // SyndicationFeed syndicationFeed{ co_await async_op_with_progress };
}

Как следует из комментария «coroutine» выше, вместо того чтобы использовать делегат с событиями завершения асинхронных действий и операций, вам, вероятно, будет удобнее использовать корутины. Дополнительные сведения и примеры кода см. в разделе Параллелизм и асинхронные операции с C++/WinRT.

Note

Некорректно реализовать несколько обработчиков завершения для асинхронного действия или операции. У вас может быть один делегат для завершенного события или его можно co_await . Если у вас есть оба, то второй завершится ошибкой.

Если вы придерживаетесь делегатов вместо корутины, вы можете выбрать более простой синтаксис.

async_op_with_progress.Completed(
    [](auto&& /*sender*/, AsyncStatus const /* args */)
{
    // ...
});

Типы делегатов, возвращающие значение

Некоторые типы делегатов сами должны возвращать значение. Примером является ListViewItemToKeyHandler, который возвращает строку. Ниже приведен пример создания делегата этого типа (обратите внимание, что лямбда-функция возвращает значение).

using namespace winrt::Microsoft::UI::Xaml::Controls;

winrt::hstring f(ListView listview)
{
    return ListViewPersistenceHelper::GetRelativeScrollPosition(listview, [](IInspectable const& item)
    {
        return L"key for item goes here";
    });
}

Безопасный доступ к этому указателю с помощью делегата обработки событий

Если вы обрабатываете событие с функцией-членом объекта или из лямбда-функции внутри функции-члена объекта, необходимо подумать о относительных времени существования получателя события (объект, обрабатывающий событие) и источнике событий (объект, вызывающий событие). Дополнительные сведения и примеры кода см. в разделе "Сильные и слабые ссылки" в C++/WinRT.

Важные API