Replies: 1 comment
-
This example shows how to generate icons from Flutter widgets. Code Sample
import 'dart:async';
import 'dart:ffi' as ffi;
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:win32/win32.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GlucoseControls(color: Colors.red),
GlucoseControls(color: Colors.green),
GlucoseControls(color: Colors.blue),
],
),
),
);
}
}
class GlucoseControls extends StatefulWidget {
final Color color;
const GlucoseControls({super.key, required this.color});
@override
State<GlucoseControls> createState() => _GlucoseControlsState();
}
class _GlucoseControlsState extends State<GlucoseControls> with WidgetsBindingObserver {
GlobalKey indicatorKey = GlobalKey();
var glucose = 1.0;
final trayIconGlucose = TrayIcon('Glucose');
var isDispose = false;
@override
void initState() {
updateGlucoseTrayIcon();
super.initState();
}
@override
void dispose() {
isDispose = true;
trayIconGlucose.removeIcon();
super.dispose();
}
@override
void reassemble() {
super.reassemble();
updateGlucoseTrayIcon();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GlucoseIndicator(
key: indicatorKey,
color: widget.color,
glucose: glucose,
),
Slider(
max: 6.0,
value: glucose,
onChanged: (newVal) {
setState(() {
glucose = newVal;
});
updateGlucoseTrayIcon();
},
),
ElevatedButton(
child: const Text('Remove Icon'),
onPressed: () {
trayIconGlucose.removeIcon();
},
),
],
);
}
var trayLastTaskUpdateId = -1;
void updateGlucoseTrayIcon() {
trayLastTaskUpdateId = Random().nextInt(1000000);
final thisId = trayLastTaskUpdateId;
Future.delayed(const Duration(milliseconds: 200), () {
if (thisId != trayLastTaskUpdateId) {
return;
}
if (isDispose) {
return;
}
CaptureWidget(indicatorKey)
.toIcon()
.then((value) => trayIconGlucose.icon = value);
});
}
}
class GlucoseIndicator extends StatelessWidget {
final double glucose;
final Color color;
const GlucoseIndicator({
Key? key,
required this.glucose,
required this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Container(
alignment: Alignment.center,
width: 200,
height: 200,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Container(
alignment: Alignment.bottomCenter,
color: Colors.white,
width: 100,
height: 200,
),
Container(
alignment: Alignment.center,
width: 100,
height: 200 / 6 * glucose,
color: color,
),
Center(
child: Text(
glucose.toStringAsFixed(0),
style: const TextStyle(
height: 0,
fontSize: 150,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}
class CaptureWidget {
final GlobalKey widgetKey;
CaptureWidget(this.widgetKey);
Future<int> toIcon() async {
const iconWidth = 16;
const iconSize = iconWidth / 200;
final bytes = await getWidgetPixels(iconSize);
return convertToIcon(bytes!, iconWidth);
}
Future<ByteData?> getWidgetPixels(double iconSize) async {
final boundary =
widgetKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: iconSize);
final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
return bytes;
}
int convertToIcon(ByteData sourceData, int iconWidth) {
return using((arena) {
final pixelLen = iconWidth * iconWidth;
final pixelsInBytes = pixelLen * 4;
final targetBytes = arena<ffi.Uint8>(pixelsInBytes);
final sourceBytes = sourceData.buffer.asUint8List();
if (pixelsInBytes != sourceData.lengthInBytes) {
throw 'Icon size($pixelLen) and source data size'
'(${sourceData.lengthInBytes}) should be equals';
}
memCopyAndConvertToBGRA(pixelsInBytes, targetBytes, sourceBytes);
return convertPixelsToHICON(iconWidth, targetBytes);
});
}
void memCopyAndConvertToBGRA(
int pixelsInBytes,
ffi.Pointer<ffi.Uint8> target,
Uint8List source,
) {
const r = 0, g = 1, b = 2, a = 3;
for (var i = 0; i < pixelsInBytes; i += 4) {
target[i + r] = source[i + b];
target[i + g] = source[i + g];
target[i + b] = source[i + r];
target[i + a] = source[i + a];
}
}
int convertPixelsToHICON(int iconWidth, ffi.Pointer<ffi.Uint8> target) {
return using((arena) {
final info = arena<ICONINFO>();
try {
info
..ref.hbmColor = CreateBitmap(iconWidth, iconWidth, 1, 32, target)
..ref.hbmMask = CreateBitmap(iconWidth, iconWidth, 1, 1, ffi.nullptr);
return CreateIconIndirect(info);
} finally {
DeleteObject(info.ref.hbmColor);
DeleteObject(info.ref.hbmMask);
}
});
}
}
class TrayIcon {
final String title;
TrayIcon(this.title);
// ignore: constant_identifier_names
static const EVENT_TRAY_NOTIFY = WM_APP + 1;
set icon(int newIcon) {
update(newIcon);
}
void update(int icon) {
if (_nid == null) {
addIcon();
}
_nid!.ref.hIcon = icon;
Shell_NotifyIcon(NIM_MODIFY, _nid!);
}
static var uID = 1;
final _uID = uID++;
void addIcon() {
if (_nid != null) {
removeIcon();
}
_nid = calloc<NOTIFYICONDATA>();
_nid!.ref
..cbSize = ffi.sizeOf<NOTIFYICONDATA>()
..uID = _uID
..hWnd = findMainWindow()!
..uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE | NIF_SHOWTIP
..szTip = title
..uCallbackMessage = EVENT_TRAY_NOTIFY
..hIcon = 0;
Shell_NotifyIcon(NIM_ADD, _nid!);
_nid!.ref.uVersion = 4;
Shell_NotifyIcon(NIM_SETVERSION, _nid!);
}
void removeIcon() {
if (_nid == null) {
return;
}
Shell_NotifyIcon(NIM_DELETE, _nid!);
//DeleteObject(_nid!.ref.hIcon);
calloc.free(_nid!);
_nid = null;
}
ffi.Pointer<NOTIFYICONDATA>? _nid;
}
int? findMainWindow() {
final processId = GetCurrentProcessId();
final data = calloc<EnumWindowsData>()
..ref.hwnd = 0
..ref.processId = processId;
final callback = ffi.Pointer.fromFunction<EnumWindowsProc>(
_enumWindowsCallback,
0,
);
try {
do {
EnumWindows(callback, data.address);
} while (data.ref.hwnd == 0);
} finally {
free(data);
}
return data.ref.hwnd == 0 ? null : data.ref.hwnd;
}
final class EnumWindowsData extends ffi.Struct {
@ffi.IntPtr()
external int hwnd;
@ffi.IntPtr()
external int processId;
}
int _enumWindowsCallback(int hwnd, int lParam) {
final data = ffi.Pointer<EnumWindowsData>.fromAddress(lParam);
final processId = calloc<ffi.Uint32>();
try {
GetWindowThreadProcessId(hwnd, processId);
if (processId.value != data.ref.processId) {
return TRUE;
}
} finally {
free(processId);
}
final own = GetWindow(hwnd, GW_OWNER);
if (own != 0) {
return TRUE;
}
data.ref.hwnd = hwnd;
return FALSE; // stop enums
}
7hyB6wnGVx.mp4 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hi,
can you help me realizing a very special use case?
I am looking for a way how to create two Windows system tray icons from a Flutter app.
Futermore I like to use dynamic icons instead of static icons read from the filey system: The app should display the actual blood glucose level plus a trend indicator. So, I need to dynamically generate the blood glucose icon from text and the trend indicator arrow needs to be rotated to visualize the correct trend indication.
Thank you for your ideas and suggestions,
Arne
Beta Was this translation helpful? Give feedback.
All reactions