/window_plus

As it should be. Extend view into title-bar.

Primary LanguageDartMIT LicenseMIT

Work in progress. API may change.

As it should be. Extend view into title-bar.

0 1

Windows 7

3

Features

  • Remembering window position & state at application launch & quit.
  • Frameless & customizable title-bar on Windows 10 RS1 or higher with correct resize & move hit-box.
  • Excellent backward compatibility, till Windows 7 SP1.
  • Fullscreen support.
  • Overlay & always on-top support.
  • Programmatic maximize, restore, size, move, close & destroy.
  • Subscription to window resize, move, minimize, maximize & fullscreen events.
  • Customizable minimum window size.
  • Multiple monitor(s) compatibility.
  • Single instance support & argument vector (List<String> args) forwarding.
  • Windows 11 snap layouts.
  • Interception of window close event e.g. for code execution or clean-up before application quit.
  • Well tested & stable as fuck.

Reference

Initializing the plugin

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  /// Ideally, should be present right after [WidgetsFlutterBinding.ensureInitialized] & anywhere before [runApp].
  await WindowPlus.ensureInitialized(
    /// Pass a unique identifier for your application.
    application: 'com.alexmercerind.window_plus',
    /// Optional: 
    enableCustomFrame: true,     // true by default on Windows 10 RS1 or higher.
    enableEventStreams: false,    // true by default.
  );
}

Intercepting window close event

WindowPlus.instance.setWindowCloseHandler(() async {
  /// Show alert to the user. Likely if some operation is pending.
  /// Perform clean-up.
  final bool canWindowClose = await doSomethingBeforeClose();
  return canWindowClose;
});

Receiving single instance arguments

WindowPlus.instance.setSingleInstanceArgumentsHandler((List<String> args) {
  print(args.toString());
});

Entering or leaving fullscreen

WindowPlus.instance.setIsFullscreen(true);

Programmatically controlling window

/// Control window state.

WindowPlus.instance.minimize();
WindowPlus.instance.maximize();
WindowPlus.instance.restore();

WindowPlus.instance.move(40, 40);
WindowPlus.instance.resize(640, 480);

WindowPlus.instance.show();
WindowPlus.instance.hide();

/// Close the window.
/// [WindowPlus.instance.setWindowCloseHandler] may be used to intercept the action.

WindowPlus.instance.close();

/// Closes the window without respecting the [WindowPlus.instance.setWindowCloseHandler] handler.

WindowPlus.instance.destroy();

/// Query.
final bool maximized = await WindowPlus.instance.maximized;
final bool minimized = await WindowPlus.instance.minimized;
final bool fullscreen = await WindowPlus.instance.fullscreen;
final Rect size = await WindowPlus.instance.size;
final Offset position = await WindowPlus.instance.position;

Fetching available monitors

/// Get all the available monitors.

final List<Monitor> monitors = await WindowPlus.instance.monitors;

Subscribing to window events

WindowPlus.instance.maximizedStream.listen((bool value) {
  print(value.toString());
});

WindowPlus.instance.minimizedStream.listen((bool value) {
  print(value.toString());
});

WindowPlus.instance.fullscreenStream.listen((bool value) {
  print(value.toString());
});

WindowPlus.instance.sizeStream.listen((Rect size) {
  print(size.toString());
});

WindowPlus.instance.positionStream.listen((Offset position) {
  print(position.toString());
});

Displaying custom title-bar

1. Default Windows look.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        /// Use a [Stack] to make your app's content to "bleed through the title-bar" & give a seamless look.
        body: Stack(
          alignment: Alignment.topCenter,
          children: [
            /// Actual application content.
            MyScreen(),
            /// Window title-bar that follows Windows' default design.
            /// It's height can be accessed using [WindowPlus.instance.captionHeight].
            /// Only shows on Windows 10 or higher. On lower Windows versions, the default window frame is kept. Thus, no need for rendering second one.
            WindowCaption(
              /// Optionally, [brightness] may be set to make window controls white or black (as default Windows 10+ design does).
              /// By default, this is decided by [MediaQuery].
              brightness: Brightness.dark,
              /// A [child] may be passed to render custom content in the title-bar.
            ),
          ],
        ),
      ),
    );
  }

2. Custom look.

You may compose your own window title-bar & controls. See following widgets for reference:

  • WindowCaptionArea
  • WindowMinimizeButton
  • WindowMaximizeButton
  • WindowRestoreButton
  • WindowCloseButton
  • WindowRestoreMaximizeButton

Setup

Following configuration is required.

Windows

Edit windows/runner/win32_window.cpp as:

   HWND window = CreateWindow(
-      window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
       Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
       Scale(size.width, scale_factor), Scale(size.height, scale_factor),
       nullptr, nullptr, GetModuleHandle(nullptr), this);
-    case WM_SIZE: {
-      RECT rect = GetClientArea();
-      if (child_content_ != nullptr) {
-        // Size and position the child window.
-        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
-                   rect.bottom - rect.top, TRUE);
-      }
-      return 0;
-    }

     case WM_ACTIVATE:

Linux

Edit linux/my_application.cc as:

-  gtk_widget_show(GTK_WIDGET(window));
+  gtk_widget_realize(GTK_WIDGET(window));
   g_autoptr(FlDartProject) project = fl_dart_project_new();
   fl_dart_project_set_dart_entrypoint_arguments(
       project, self->dart_entrypoint_arguments);
   FlView* view = fl_view_new(project);
-  gtk_widget_show(GTK_WIDGET(view));
+  gtk_widget_realize(GTK_WIDGET(view));
   gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
   fl_register_plugins(FL_PLUGIN_REGISTRY(view));
-  gtk_widget_grab_focus(GTK_WIDGET(view));

Single Instance

For enabling single instance support, follow the steps below.

Windows

In windows/runner/main.cpp, add the following code:

  #include <flutter/dart_project.h>
  #include <flutter/flutter_view_controller.h>
  #include <windows.h>

  #include "flutter_window.h"
  #include "utils.h"
+ #include "window_plus/window_plus_plugin_c_api.h"

  int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
                        _In_ wchar_t* command_line, _In_ int show_command) {
+   ::WindowPlusPluginCApiHandleSingleInstance(NULL, NULL);

    // Attach to console when present (e.g., 'flutter run') or create a
    // new console when running with a debugger.
    if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
      CreateAndAttachConsole();
    }

If you use custom window class name, then you can pass it as the first argument instead of NULL. Similarly, if you want to also account for your window's title, then you can pass it as the second argument instead of NULL.

Linux

You need to edit your linux/my_application.cc file. See this file for reference.

Notice how G_APPLICATION_HANDLES_OPEN & G_APPLICATION_HANDLES_COMMAND_LINE are implemented.

Finally, forward the arguments to Dart / Flutter, with window_plus_plugin_handle_single_instance call at the required location.

Platforms

  • Windows
  • Linux

Why

package:window_plus is made to leverage requirements of Harmonoid.

Initially, Harmonoid used package:bitsdojo_window for a modern-looking window on Windows. However, as time went by a number of issues were faced like:

  • Resize hit-box inside window (which made Widgets near window borders hard to interract e.g. scrollbar)
  • Windows 7 support.
  • Other stability & crash issues.

This gave birth to my fork of package:bitsdojo_window, where I fixed various issues I discovered. However, after mending things in a dirty manner (partially due to the fact that my style of writing code is different), the code became really spaghetti & now it's something I can no longer trust. Thus, I decided to create package:window_plus which is far more cleaner (follows Google C++ Style Guide), correctly implemented & offers additional features like:

  • Ability to intercept window close event.
  • Remembering window position & state.
  • Fullscreen support.

I also didn't want a custom frame on GNU/Linux version of Harmonoid, since it's "not the trend". See: Discord, Visual Studio Code or Spotify. I believe ensuring compatibility with all desktop environments like KDE, XFCE, GNOME & other tiling ones is far more important. So, best is to customize the native window behavior as less as possible on Linux. On the other hand, most GNU/Linux desktop environments offer various customization options for changing window controls' style/position, window's frame/border etc. anyway. This functionality of host OS would be unusable after implementing a custom frame & rendering custom title bar with Flutter.

Stability & correct implementation is the primary concern here.

Now, i.e. package:window_plus can serve as a starting point for applications other than Harmonoid.

License

MIT License

Copyright © 2022, Hitesh Kumar Saini saini123hitesh@gmail.com.

It's free real estate.