Introduction
In this blog post, we will build a Task Manager app in Flutter using the win32 package. By utilizing the Windows APIs provided by win32, we'll create an intuitive app to view and manage running tasks on a Windows system.
Whether you're a developer looking to enhance your Flutter skills or an enthusiast eager to dive into Windows programming, this guide will walk you through the process of creating your own Task Manager app from scratch.
Here's what we'll cover:
- Feature Overview
- Setting Up the Project
- Defining the Models
- Implementing Task Manager Logic
- Building the UI
- Conclusion
- Source Code
Feature Overview
Our Task Manager app will include the following key features:
- Enumerating running tasks: View a list of running tasks, including their names, PIDs, and descriptions.
- Searching and sorting tasks: Search and sort tasks based on their name, PID, or description.
- Starting a new task: Start a new task by specifying its executable path directly within the app.
- Terminating a task: Terminate a running task by clicking the button next to the task.
Setting Up the Project
Before we dive into coding, let’s set up our project.
Creating a New Flutter Project
Open your terminal and run:
flutter create task_manager --platforms=windows
cd task_manager
Installing Dependencies
Add the ffi and win32 packages to your project with:
flutter pub add ffi win32
Defining the Models
We'll start by defining the models responsible for storing task information and sorting options.
Create a new file named models.dart
in the lib\src
directory and add the
following code:
import 'dart:typed_data';
/// Specifies the field by which to sort the tasks.
enum SortBy {
/// Sort by task name.
name,
/// Sort by task PID (Process ID).
pid,
/// Sort by task description.
description,
}
/// Specifies the order in which to sort the tasks.
enum SortOrder {
/// Sort in ascending order.
ascending,
/// Sort in descending order.
descending,
}
/// A Windows task with its icon, name, PID, and description.
class Task {
const Task({
required this.iconAsBytes,
required this.name,
required this.pid,
required this.description,
});
/// The icon of the task.
final Uint8List iconAsBytes;
/// The name of the task.
final String name;
/// The PID (Process ID) of the task.
final int pid;
/// The description of the task.
final String description;
}
Implementing Task Manager Logic
Next, we'll implement the functionality for managing Windows tasks, including enumerating running tasks, starting new tasks, and terminating tasks.
Create a new file named task_manager.dart
in the lib\src
directory and
add the following code to set up the skeleton for managing Windows tasks:
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
import 'models.dart';
/// Provides functionality for managing Windows tasks, including:
/// - Enumerating running tasks
/// - Running a new task
/// - Terminating a running task
abstract class TaskManager {
/// Runs a new task from the specified [path].
///
/// Returns `true` if the task was successfully started; otherwise, `false`.
static bool run(String path) {
// TODO: Implement this method
throw UnimplementedError();
}
/// Retrieves a list of currently running tasks.
///
/// Returns `null` if retrieval failed.
static List<Task>? get tasks {
// TODO: Implement this method
throw UnimplementedError();
}
/// Terminates a running task with the given [pid].
///
/// Returns `true` if the task was successfully terminated; otherwise,
/// `false`.
static bool terminate(int pid) {
// TODO: Implement this method
throw UnimplementedError();
}
}
With the skeleton in place, we can start implementing the task manager logic.
Running a New Task
Now, let's implement the run
function to run a new task.
/// Runs a new task from the specified [path].
///
/// Returns `true` if the task was successfully started; otherwise, `false`.
static bool run(String path) {
final lpFile = path.toNativeUtf16();
final result = ShellExecute(
0,
'open'.toNativeUtf16(),
lpFile,
nullptr,
nullptr,
SHOW_WINDOW_CMD.SW_SHOWNORMAL,
);
free(lpFile);
return result > 32;
}
We first convert the provided file path into a native UTF-16 format using the
toNativeUtf16
extension method from package:ffi
. This formatted path is then
passed along with other necessary parameters to ShellExecute, specifying an
action to open the file and dictate how the new process window should appear.
After executing the function, we free the allocated memory for the path to
ensure efficient resource management. If the value returned by ShellExecute is
greater than 32, it indicates a successful task launch, and the function
returns true
. Otherwise, it returns false
.
Enumerating Running Tasks
Next, we'll implement the tasks
getter to enumerate all running tasks on
the system.
/// Retrieves a list of currently running tasks.
///
/// Returns `null` if retrieval failed.
static List<Task>? get tasks {
return using((arena) {
final tasks = <Task>[];
final buffer = arena<Uint32>(1024);
final cbNeeded = arena<Uint32>();
if (EnumProcesses(buffer, sizeOf<Uint32>() * 1024, cbNeeded) == FALSE) {
return null;
}
final processCount = cbNeeded.value ~/ sizeOf<Uint32>();
final processIds = buffer.asTypedList(processCount);
for (final pid in processIds) {
final hProcess = OpenProcess(
PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION |
PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ,
FALSE,
pid,
);
if (hProcess != NULL) {
final hModule = arena<HMODULE>();
final cbNeededMod = arena<Uint32>();
if (EnumProcessModules(
hProcess, hModule, sizeOf<HMODULE>(), cbNeededMod) !=
0) {
final moduleName = arena<WCHAR>(MAX_PATH).cast<Utf16>();
if (GetModuleBaseName(
hProcess,
hModule.value,
moduleName,
MAX_PATH,
) >
0) {
final name = moduleName.toDartString();
final filePath = arena<WCHAR>(MAX_PATH).cast<Utf16>();
final result = GetModuleFileNameEx(
hProcess, hModule.value, filePath, MAX_PATH);
final path = result != 0 ? filePath.toDartString() : null;
final description =
path != null ? (_getFileDescription(path) ?? name) : name;
final task = Task(
iconAsBytes: path != null
? (_extractIcon(path) ?? Uint8List(0))
: Uint8List(0),
name: name,
pid: pid,
description: description,
);
tasks.add(task);
}
}
CloseHandle(hProcess);
}
}
return tasks;
});
}
We begin by allocating memory for an array of Uint32
values to store the PIDs
of running processes. We then call EnumProcesses to retrieve the list of PIDs
and the number of processes.
Next, we iterate over the list of PIDs and open a handle to each process using OpenProcess. We then call EnumProcessModules to retrieve the module handle for the process and GetModuleBaseName to retrieve the name of the module.
Next, we retrieve the file path of the module using GetModuleFileNameEx and
extract the file description using the _getFileDescription
function. We also
extract the icon of the task using the _extractIcon
function. Finally, we
create a Task
object with the retrieved information and add it to the list of
tasks.
Retrieving File Description
Next, we'll implement the _getFileDescription
function to retrieve the file
description.
static String? _getFileDescription(String path) {
return using((arena) {
final lptstrFileName = path.toNativeUtf16(allocator: arena);
final handle = arena<Uint32>();
final size = GetFileVersionInfoSize(lptstrFileName, handle);
if (size == 0) return null;
final versionInfo = arena<Uint8>(size);
if (GetFileVersionInfo(lptstrFileName, 0, size, versionInfo) == FALSE) {
return null;
}
final lplpBuffer = arena<Pointer<Utf16>>();
final puLen = arena<Uint32>();
if (VerQueryValue(
versionInfo,
r'\StringFileInfo\040904b0\FileDescription'
.toNativeUtf16(allocator: arena),
lplpBuffer.cast(),
puLen,
) ==
FALSE) {
return null;
}
if (puLen.value == 0) return null;
return lplpBuffer.value.toDartString();
});
}
We first convert the provided file path into a native UTF-16 format using the
toNativeUtf16
extension method from package:ffi
. This formatted path is then
passed to GetFileVersionInfoSize to retrieve the size of the version
information block for the specified file.
Next, we allocate memory for the version information block and call GetFileVersionInfo to retrieve the version information for the file.
We then use VerQueryValue to retrieve the file description from the version
information block. If the value is 0
, the function returns null
. Otherwise,
it converts the retrieved value to a Dart string and returns it.
Extracting Task Icon
Finally, we'll implement the _extractIcon
function to extract the icon of the
task.
static Uint8List? _extractIcon(String path) {
return using((arena) {
final filePath = path.toNativeUtf16(allocator: arena);
final instance = GetModuleHandle(nullptr);
final iconID = arena<WORD>();
final hIcon = ExtractAssociatedIcon(instance, filePath, iconID);
if (hIcon == NULL) return null;
return _getIconData(hIcon);
});
}
static Uint8List? _getIconData(int hIcon, {int nColorBits = 32}) {
return using((arena) {
final buffer = <int>[];
final hdc = CreateCompatibleDC(NULL);
final icoHeader = [0, 0, 1, 0, 1, 0];
buffer.addAll(icoHeader);
final iconInfo = arena<ICONINFO>();
if (GetIconInfo(hIcon, iconInfo) == 0) {
DeleteDC(hdc);
return null;
}
final bmInfo = arena<BITMAPINFO>();
bmInfo.ref.bmiHeader
..biSize = sizeOf<BITMAPINFOHEADER>()
..biBitCount = 0;
if (GetDIBits(
hdc,
iconInfo.ref.hbmColor,
0,
0,
nullptr,
bmInfo,
DIB_USAGE.DIB_RGB_COLORS,
) ==
0) {
DeleteDC(hdc);
return null;
}
int nBmInfoSize = sizeOf<BITMAPINFOHEADER>();
if (nColorBits < 24) {
nBmInfoSize += sizeOf<RGBQUAD>() * (1 << nColorBits);
}
if (bmInfo.ref.bmiHeader.biSizeImage == 0) {
DeleteDC(hdc);
return null;
}
final bits = arena<Uint8>(bmInfo.ref.bmiHeader.biSizeImage);
bmInfo.ref.bmiHeader
..biBitCount = nColorBits
..biCompression = BI_COMPRESSION.BI_RGB;
if (GetDIBits(
hdc,
iconInfo.ref.hbmColor,
0,
bmInfo.ref.bmiHeader.biHeight,
bits,
bmInfo,
DIB_USAGE.DIB_RGB_COLORS,
) ==
0) {
DeleteDC(hdc);
return null;
}
final maskInfo = arena<BITMAPINFO>();
maskInfo.ref.bmiHeader
..biSize = sizeOf<BITMAPINFOHEADER>()
..biBitCount = 0;
if (GetDIBits(
hdc,
iconInfo.ref.hbmMask,
0,
0,
nullptr,
maskInfo,
DIB_USAGE.DIB_RGB_COLORS,
) ==
0 ||
maskInfo.ref.bmiHeader.biBitCount != 1) {
DeleteDC(hdc);
return null;
}
final maskBits = arena<Uint8>(maskInfo.ref.bmiHeader.biSizeImage);
if (GetDIBits(
hdc,
iconInfo.ref.hbmMask,
0,
maskInfo.ref.bmiHeader.biHeight,
maskBits,
maskInfo,
DIB_USAGE.DIB_RGB_COLORS,
) ==
0) {
DeleteDC(hdc);
return null;
}
final dir = arena<_IconDirectoryEntry>();
dir.ref
..nWidth = bmInfo.ref.bmiHeader.biWidth
..nHeight = bmInfo.ref.bmiHeader.biHeight
..nNumColorsInPalette = (nColorBits == 4 ? 16 : 0)
..nNumColorPlanes = 0
..nBitsPerPixel = bmInfo.ref.bmiHeader.biBitCount
..nDataLength = bmInfo.ref.bmiHeader.biSizeImage +
maskInfo.ref.bmiHeader.biSizeImage +
nBmInfoSize
..nOffset = sizeOf<_IconDirectoryEntry>() + 6;
buffer
.addAll(dir.cast<Uint8>().asTypedList(sizeOf<_IconDirectoryEntry>()));
bmInfo.ref.bmiHeader
..biHeight *= 2
..biCompression = 0
..biSizeImage += maskInfo.ref.bmiHeader.biSizeImage;
buffer.addAll(bmInfo.cast<Uint8>().asTypedList(nBmInfoSize));
buffer.addAll(bits.asTypedList(bmInfo.ref.bmiHeader.biSizeImage));
buffer.addAll(maskBits.asTypedList(maskInfo.ref.bmiHeader.biSizeImage));
DeleteObject(iconInfo.ref.hbmColor);
DeleteObject(iconInfo.ref.hbmMask);
DeleteDC(hdc);
return Uint8List.fromList(buffer);
});
}
base class _IconDirectoryEntry extends Struct {
()
external int nWidth;
()
external int nHeight;
()
external int nNumColorsInPalette;
()
external int nReserved;
()
external int nNumColorPlanes;
()
external int nBitsPerPixel;
()
external int nDataLength;
()
external int nOffset;
}
We first convert the provided file path into a native UTF-16 format using the
toNativeUtf16
extension method from package:ffi
. This formatted path is then
passed to ExtractAssociatedIcon to retrieve the handle to the associated icon
for the specified file.
Next, we call the _getIconData
function to extract the icon data from the icon
handle. This function retrieves the icon information, including the icon size,
color depth, and pixel data, and returns it as a Uint8List
.
Terminating a Task
Finally, let's implement the terminate
function to
terminate a running task.
/// Terminates a running task with the given [pid].
///
/// Returns `true` if the task was successfully terminated; otherwise,
/// `false`.
static bool terminate(int pid) {
final handle =
OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE, FALSE, pid);
if (handle == NULL) return false;
try {
return TerminateProcess(handle, 0) == TRUE;
} finally {
CloseHandle(handle);
}
}
We first attempt to open a handle to the process with the specified PID using
OpenProcess. If the handle is successfully opened, we proceed to terminate the
process by calling TerminateProcess. If the termination is successful, the
function returns true
; otherwise, it returns false
. Finally, we close the
handle to the process using CloseHandle
to ensure proper cleanup.
Building the UI
With the task manager logic in place, we can now focus on building the UI for our Task Manager app.
Setting Up the Main Entry Point
First, open lib\main.dart
file and replace the contents with the following
code to set up the main entry point for the app:
import 'package:flutter/material.dart';
import 'models.dart';
import 'task_manager.dart';
void main() {
runApp(const TaskManagerApp());
}
class TaskManagerApp extends StatelessWidget {
const TaskManagerApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Task Manager',
theme: ThemeData(
brightness: Brightness.dark,
),
home: const TaskManagerHomeScreen(),
);
}
}
Creating the Home Screen Skeleton
Next, let's create the basic structure of the home screen including the StatefulWidget and State class.
class TaskManagerHomeScreen extends StatefulWidget {
const TaskManagerHomeScreen({super.key});
TaskManagerHomeScreenState createState() => TaskManagerHomeScreenState();
}
class TaskManagerHomeScreenState extends State<TaskManagerHomeScreen> {
var _tasks = <Task>[];
var _filteredTasks = <Task>[];
int? _selectedTask;
var _sortBy = SortBy.name;
var _sortOrder = SortOrder.ascending;
TextEditingController? _searchController;
FocusNode? _searchFocusNode;
void initState() {
super.initState();
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
loadTasks();
}
void dispose() {
_searchController?.dispose();
_searchFocusNode?.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Task Manager'),
actions: [],
),
body: const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('No tasks found'),
),
),
);
}
}
Loading and Displaying Tasks
Now, let's implement the method to load tasks, update the state, and use the
DataTable
widget to display tasks.
void loadTasks() {
setState(() {
_tasks = TaskManager.tasks ?? [];
_filteredTasks = List.from(_tasks);
sortTasks();
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: // ...
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _filteredTasks.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('No tasks found'),
),
)
: DataTable(
columns: [
const DataColumn(label: Text('Name')),
const DataColumn(label: Text('PID'), numeric: true),
const DataColumn(label: Text('Description')),
const DataColumn(label: Text('Actions')),
],
rows: _filteredTasks.map((task) {
return DataRow(
cells: [
DataCell(Text(task.name)),
DataCell(Text(task.pid.toString())),
DataCell(Text(task.description)),
DataCell(
IconButton(
icon: const Icon(
Icons.cancel_outlined,
color: Colors.red,
),
onPressed: () {},
),
),
],
);
}).toList(),
),
),
],
),
);
}
Task Sorting, Searching, and Refreshing
Next, let's implement the functionality to sort and search tasks based on the user's input and add a button to the app bar to refresh the task list.
void searchTasks(String query) {
final filtered = _tasks.where((task) {
return task.name.toLowerCase().contains(query.toLowerCase()) ||
task.description.toLowerCase().contains(query.toLowerCase()) ||
task.pid.toString().contains(query);
}).toList();
setState(() {
_filteredTasks = filtered;
sortTasks();
});
}
void sortTasks() {
setState(() {
_filteredTasks.sort((a, b) {
final cmp = switch (_sortBy) {
SortBy.name => a.name.compareTo(b.name),
SortBy.pid => a.pid.compareTo(b.pid),
SortBy.description => a.description.compareTo(b.description),
};
return _sortOrder == SortOrder.ascending ? cmp : -cmp;
});
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Task Manager'),
actions: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Tooltip(
message: 'Type a name or PID to search',
child: SizedBox(
height: 40,
width: 300,
child: TextField(
controller: _searchController,
focusNode: _searchFocusNode,
decoration: InputDecoration(
labelText: 'Type a name or PID to search',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.search),
suffix: _searchController!.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_searchController!.clear();
_searchFocusNode!.unfocus();
_tasks = TaskManager.tasks ?? [];
_filteredTasks = List.from(_tasks);
sortTasks();
});
},
)
: null,
),
onChanged: searchTasks,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
_tasks = TaskManager.tasks ?? [];
if (_searchController!.text.isNotEmpty) {
searchTasks(_searchController!.text);
} else {
_filteredTasks = List.from(_tasks);
sortTasks();
}
});
},
tooltip: 'Refresh the list of tasks',
),
),
],
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _filteredTasks.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('No tasks found'),
),
)
: DataTable(
columns: [
DataColumn(
label: const Text('Name'),
onSort: (columnIndex, ascending) {
setState(() {
_sortBy = SortBy.name;
_sortOrder = ascending
? SortOrder.ascending
: SortOrder.descending;
sortTasks();
});
},
),
DataColumn(
label: const Text('PID'),
numeric: true,
onSort: (columnIndex, ascending) {
setState(() {
_sortBy = SortBy.pid;
_sortOrder = ascending
? SortOrder.ascending
: SortOrder.descending;
sortTasks();
});
},
tooltip: 'Process ID',
),
DataColumn(
label: const Text('Description'),
onSort: (columnIndex, ascending) {
setState(() {
_sortBy = SortBy.description;
_sortOrder = ascending
? SortOrder.ascending
: SortOrder.descending;
sortTasks();
});
},
),
const DataColumn(label: Text('Actions')),
],
rows: // ...
sortAscending: _sortOrder == SortOrder.ascending,
sortColumnIndex: switch (_sortBy) {
SortBy.name => 0,
SortBy.pid => 1,
SortBy.description => 2,
},
),
),
],
),
);
}
Task Termination
Next, let's implement the functionality to terminate a task. We'll display a confirmation dialog to user before terminating the task.
void confirmEndTask(int pid, String taskName) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Do you want to end $taskName?'),
content: const Text(
'If an open program is associated with this process, it will close '
'and you will lose any unsaved data. If you end a system process, '
'it might result in system instability. Are you sure you want to '
'continue?',
),
actions: <Widget>[
TextButton(
child: const Text('End task'),
onPressed: () {
Navigator.of(context).pop();
if (TaskManager.terminate(pid)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Task "$taskName" ended successfully'),
),
);
loadTasks();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to end task "$taskName"'),
),
);
}
},
),
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: // ...
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _filteredTasks.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('No tasks found'),
),
)
: DataTable(
columns: // ...
rows: _filteredTasks.map((task) {
return DataRow(
cells: [
// ...
DataCell(
IconButton(
icon: const Icon(
Icons.cancel_outlined,
color: Colors.red,
),
onPressed: () => confirmEndTask(task.pid, task.name),
),
),
],
);
}).toList(),
// ...
),
),
],
),
);
}
Task Creation
Finally, let's implement the functionality to run a new task by displaying a dialog with a text field to the user for entering the task name.
void runTask(String path) {
final result = TaskManager.run(path);
if (result) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Task "$path" started successfully'),
),
);
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Error'),
content: Text('Failed to run task "$path"'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Ok'),
),
],
);
},
);
}
}
void showRunTaskDialog() {
final taskNameController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Run new task'),
content: TextField(
autofocus: true,
onSubmitted: (_) {
final path = taskNameController.text;
if (path.isNotEmpty) {
runTask(path);
}
},
controller: taskNameController,
decoration: const InputDecoration(hintText: 'Enter task name'),
),
actions: [
TextButton(
onPressed: () {
final path = taskNameController.text;
if (path.isNotEmpty) {
runTask(path);
}
},
child: const Text('Run'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
],
);
},
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Task Manager'),
actions: [
// ...
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
onPressed: showRunTaskDialog,
icon: const Icon(Icons.add),
tooltip: 'Run a new task',
),
),
],
),
// ...
);
}
Conclusion
In this blog post, we've built an app in Flutter using the win32 package to manage running tasks on a Windows system. We've covered the process of enumerating running tasks, starting new tasks, and terminating tasks, as well as building a beautiful UI to interact with the task manager.
I hope this tutorial has inspired you to explore further and build even more advanced applications with Dart, Flutter, and win32. Your feedback and contributions are always welcome, so feel free to share your thoughts and ideas.
Happy coding! 🚀