Динамически оформляйте свое приложение Flutter для Windows, используя каналы событий для получения событий

Краткий обзор:узнайте, как настроить функцию WNDPROC callback из Flutter с помощью каналов платформы в плагине. В примере показано, как динамически изменить тему в вашем приложении.

Введение

Два месяца назад Flutter 2.10 принес стабильную поддержку Windows. Хорошо, это не новость, Flutter раньше компилировал приложения для Windows, почему это так важно сейчас? Ответ прост: стабильный, потому что теперь, когда Google заявил об этом, мы можем решить инвестировать больше нашего времени и ресурсов в разработку приложений для Windows с помощью этого фантастического инструмента.

Благодаря работе нескольких энтузиастов, многие пакеты на pub.dev поддерживают разработку под Windows, и этот список растет. Однако, как было сказано ранее, при разработке нашего блестящего нового приложения для Windows во Flutter иногда может быть что-то, что мы упускаем.

Например, нам может понадобиться, чтобы наше приложение реагировало на общесистемные события Windows и соответствующим образом меняло свое поведение. У Flutter нет готового интерфейса для этого. Чтобы узнать, как это сделать, мы реализуем Канал событий, который может сделать эти события доступными в нашем коде Dart.

Если у вас уже есть опыт написания плагинов с использованием каналов платформы, перейдите к следующему абзацу, если нет, вы можете прочитать предыдущую историю:



Пример: Чего мы хотим

Предположим, мы хотим, чтобы наше приложение получало событие каждый раз, когда пользователь включает или выключает темный режим в Windows.

Выполнение этого в приложении Win32 с использованием C++ или других «родных» языков кажется простым: нам нужно зарегистрировать обработчик для прослушивания событий Windows WM_, которые получает наше приложение:

HWND handle = GetActiveWindow();
oldProc = reinterpret_cast<WNDPROC>(GetWindowLongPtr(handle, GWLP_WNDPROC));
SetWindowLongPtr(handle, GWLP_WNDPROC, (LONG_PTR)MyWndProc);

это наш новый обработчик, который будет вызываться каждый раз, когда «приходит» сообщение Windows WM_:

LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
      {
        if (iMessage == WM_SETTINGCHANGE)
        {
          if (!lstrcmp(LPCTSTR(lParam), L"ImmersiveColorSet"))
                 {
                   ChangeOurAppTheme();
                 }
        }
        return oldProc(hWnd, iMessage, wParam, lParam);
      }

При перехвате сообщения WM_SETTINGCHANGE, если lParam указывает на строку Unicode «ImmersiveColorSet», ОС сигнализирует о том, что что-то изменилось в текущей теме пользовательского интерфейса.

Реализация, сторона флаттера

В приложении Flutter все немного по-другому. Начнем с клонирования этого репозитория и открытия проекта в нашей IDE.

Как мы знаем, использование Method Channel во Flutter похоже на вызов удаленного API, но теперь нам нужно подписаться на Stream. Для этого мы будем использовать Каналы событий, давайте откроем lib/windows_dark_mode.dart в предпочитаемой нами среде IDE:

Теперь мы собираемся добавить следующий код в класс WindowsDarkMode:

При этом мы:

  1. объявление _eventChannel экземпляром EventChannel. Это будет наше «транспортное соединение» между кодом Dart и Platform. В конструкторе мы передаем имя канала. Действительно, мы структурировали это имя как «путь», соответствующий имени нашего плагина и собственному имени, которое идентифицирует правильный поток. Я использовал это соглашение, чтобы лучше идентифицировать канал и избежать риска столкновения с другими пакетами, действительно, вы можете назвать канал как хотите.
  2. реализован статический метод DarkModeStream(), который «транслирует» события с платформы. Внутри мы вызываем метод receiveBroadcastStream(), который предоставляет Stream<dynamic>, каскадный метод map(), который сопоставляет динамическое значение с ThemeMode, и distinct(),, который устраняет дребезг событий в случае поступления двух или более последовательных одинаковых значений.

Реализация, сторона платформы

Заходим в BackEnd, открывая /windows/windows_dark_mode_plugin.cpp :

WindowsDarkModePlugin уже содержит код, реализованный в моем предыдущем уроке. При запросе он предоставляет текущий статус темного режима.

В RegisterWithRegistrar current у нас есть только объявление канала метода для получения запросов от Dart:

В том же методе нам нужно реализовать канал событий платформы:

plugin->m_event_channel = std::make_unique<flutter::EventChannel<flutter::EncodableValue>> (
          registrar->messenger (), "windows_dark_mode/dark_mode_callback",
          &flutter::StandardMethodCodec::GetInstance ()
       );

m_event_channel необходимо объявить в интерфейсе класса, private:

std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> m_event_channel;

И к ним нужно добавить:

#include <flutter/event_channel.h>
#include <mutex>

Канал событий готов, но для отправки событий ему нужен StreamHandler, который будет ставить события в очередь от платформы к движку, поэтому мы включаем в то же пространство имен наш новый класс:

Объявите переменную в интерфейсе WindowsDarkModePlugin:

MyStreamHandler<> *m_handler;

Создайте его и назначьте каналу событий в RegisterWithRegistrar,

MyStreamHandler<> *_handler=new MyStreamHandler<> ();
     plugin->m_handler = _handler;
     auto _obj_stm_handle = static_cast<flutter::StreamHandler<flutter::EncodableValue>*> (plugin->m_handler);
     std::unique_ptr<flutter::StreamHandler<flutter::EncodableValue>> _ptr {_obj_stm_handle};
     plugin->m_event_channel->SetStreamHandler (std::move (_ptr));

Теперь нам нужно перехватить сообщения WM_. Мы можем вызвать SetWindowLongPtr, чтобы установить наш обработчик WNDPROC, верно?

Но мы не будем, потому что Flutter Engine также зарегистрировал свой обработчик, и это может дать нам возможность прослушивать поток сообщений, поэтому мы будем использовать RegisterTopLevelWindowProcDelegate, метод, предоставляемый Flutter Engine на стороне платформы. Итак, мы заменим WindowsDarkModePlugin конструктор на деструктор:

Теперь мы установили обратный вызов для перехвата сообщений WM_ и отключили его при удалении экземпляра WindowsDarkMode.

Затем нам нужно изменить интерфейс класса WindowsDarkModePlugin:

  • изменение подписи конструктора для принятия параметра registrar;
  • объявление int window_proc_id = -1; в приватной секции, это идентифицирует обработчик, который мы регистрируем, будет полезно для дерегистрации;
  • объявив flutter::PluginRegistrarWindows* registrar; в приватной секции, нам нужно registrar чтобы отменить регистрацию нашего хука при удалении;
  • объявление заголовка std::optional<LRESULT> HandleWindowProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam); в приватной секции;

Затем мы заменим инстанцирование плагина в статическом методе RegisterWithRegistrar, просто добавив параметр registrar:
auto plugin = std::make_unique<WindowsDarkModePlugin>(registrar);

Мы реализуем HandleWindowProc в теле класса:

Мы вызываем метод on_callback в нашем обработчике потока, чтобы отправить результат isDarkModeAppEnabled(). Последний метод дает нам логическое значение, но мы упаковываем его, используя flutter::EncodableValue(). Это метод, который мы используем для отправки данных с платформы в Dart Side. нашего приложения, где наше логическое значение будет распаковано.

Теперь попробуем скомпилировать проект, просто чтобы проверить, хорош ли пока синтаксис, если да…

Давай попробуем

Теперь мы изменим основное тело в «example/lib/main.dart», обернув наш MaterialApp в StreamBuilder, поэтому тема нашего приложения изменится, когда мы будем переключать темы в Windows:

Нажимаем «Выполнить»:

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

Полный код вышеупомянутого туториала можно найти в этом GitHub Repository, в ветке events. Спасибо за прочтение.

Статьи по Теме: