• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dapplo / Dapplo.Windows / 22204175531

19 Feb 2026 11:16PM UTC coverage: 33.49% (-0.8%) from 34.27%
22204175531

push

github

Lakritzator
Added a new SharedMessageWindow, which handled windows messages without Windows.Forms or WPF dependencies. Much easier to use...

610 of 1950 branches covered (31.28%)

Branch coverage included in aggregate %.

0 of 129 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

1673 of 4867 relevant lines covered (34.37%)

29.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Dapplo.Windows.Messages/SharedMessageWindow.cs
1
// Copyright (c) Dapplo and contributors. All rights reserved.
2
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3

4
using Dapplo.Windows.Messages.Enumerations;
5
using Dapplo.Windows.Messages.Structs;
6
using System;
7
using System.Reactive.Disposables;
8
using System.Reactive.Linq;
9
using System.Reactive.Subjects;
10
using System.Runtime.InteropServices;
11
using System.Threading;
12

13
namespace Dapplo.Windows.Messages.Native;
14

15
/// <summary>
16
/// Provides functionality to create an observable stream of Windows messages using a message-only window on a dedicated thread.
17
/// </summary>
18
public static class SharedMessageWindow
19
{
20
    #region PInvokes
21
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
22
    private static extern ushort RegisterClassEx(ref WNDCLASSEX lpwc);
23

24
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
25
    private static extern bool UnregisterClass(string lpClassName, nint hInstance);
26

27
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
28
    private static extern nint CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, nint hWndParent, nint hMenu, nint hInstance, nint lpParam);
29

30
    [DllImport("user32")]
31
    private static extern bool GetMessage(out Msg lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
32

33
    [DllImport("user32")]
34
    private static extern bool TranslateMessage(ref Msg lpMsg);
35

36
    [DllImport("user32")]
37
    private static extern nint DispatchMessage(ref Msg lpMsg);
38

39
    [DllImport("user32")]
40
    private static extern nint DefWindowProc(nint hWnd, uint msg, nint wParam, nint lParam);
41

42
    [DllImport("user32")]
43
    private static extern void PostQuitMessage(int nExitCode);
44

45
    [DllImport("user32")]
46
    private static extern bool PostMessage(nint hWnd, uint Msg, nint wParam, nint lParam);
47

48
    [DllImport("kernel32")]
49
    private static extern nint GetModuleHandle(string? lpModuleName);
50
    #endregion
51

52
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
53
    private struct WNDCLASSEX
54
    {
55
        public uint cbSize; public uint style; public WndProc lpfnWndProc; public int cbClsExtra;
56
        public int cbWndExtra; public nint hInstance; public nint hIcon; public nint hCursor;
57
        public nint hbrBackground; public string lpszMenuName; public string lpszClassName; public nint hIconSm;
58
    }
59

60
    private static IObservable<WindowMessage>? _sharedStream;
NEW
61
    private static readonly object _lock = new();
×
NEW
62
    private static readonly BehaviorSubject<nint> _handleSubject = new(0);
×
63

64
    /// <summary>
65
    /// Gets an observable sequence of all window messages received by the application.
66
    /// </summary>
67
    /// <remarks>The returned observable is shared among all subscribers. Subscribing to this property allows
68
    /// monitoring of window messages as they occur. Unsubscribing from all observers will automatically release
69
    /// resources associated with the stream.</remarks>
70
    public static IObservable<WindowMessage> Messages
71
    {
72
        get
73
        {
NEW
74
            lock (_lock)
×
75
            {
NEW
76
                if (_sharedStream == null)
×
77
                {
NEW
78
                    _sharedStream = CreateBaseStream()
×
NEW
79
                        .Publish()
×
NEW
80
                        .RefCount();
×
81
                }
NEW
82
                return _sharedStream;
×
83
            }
NEW
84
        }
×
85
    }
86

87
    /// <summary>
88
    /// Subscribes to the shared message loop.
89
    /// </summary>
90
    /// <param name="onSetup">Invoked with the HWND when the window is created (or immediately if it already exists).</param>
91
    /// <param name="onTeardown">Invoked with the HWND when the window is destroyed OR when the subscription is disposed.</param>
92
    public static IObservable<WindowMessage> Listen(Action<nint> onSetup = null, Action<nint> onTeardown = null)
93
    {
NEW
94
        if (onSetup == null && onTeardown == null)
×
95
        {
NEW
96
            return Messages;
×
97
        }
98

NEW
99
        return Observable.Create<WindowMessage>(observer =>
×
NEW
100
        {
×
NEW
101
            nint activeHwnd = 0;
×
NEW
102
            object handleLock = new();
×
NEW
103

×
NEW
104
            // Manage the Handle Lifecycle
×
NEW
105
            var handleSubscription = _handleSubject.Subscribe(hwnd =>
×
NEW
106
            {
×
NEW
107
                lock (handleLock)
×
NEW
108
                {
×
NEW
109
                    if (hwnd != 0)
×
NEW
110
                    {
×
NEW
111
                        activeHwnd = hwnd;
×
NEW
112
                        onSetup?.Invoke(activeHwnd);
×
NEW
113
                    }
×
NEW
114
                    else if (activeHwnd != 0)
×
NEW
115
                    {
×
NEW
116
                        // Window was destroyed
×
NEW
117
                        onTeardown?.Invoke(activeHwnd);
×
NEW
118
                        activeHwnd = 0;
×
NEW
119
                    }
×
NEW
120
                }
×
NEW
121
            });
×
NEW
122

×
NEW
123
            // Pass through the messages
×
NEW
124
            var messageSubscription = Messages.Subscribe(observer);
×
NEW
125

×
NEW
126
            // Ensure teardown runs if the consumer drops the subscription early
×
NEW
127
            return Disposable.Create(() =>
×
NEW
128
            {
×
NEW
129
                lock (handleLock)
×
NEW
130
                {
×
NEW
131
                    if (activeHwnd != 0)
×
NEW
132
                    {
×
NEW
133
                        onTeardown?.Invoke(activeHwnd);
×
NEW
134
                        activeHwnd = 0;
×
NEW
135
                    }
×
NEW
136
                }
×
NEW
137
                handleSubscription.Dispose();
×
NEW
138
                messageSubscription.Dispose();
×
NEW
139
            });
×
NEW
140
        });
×
141
    }
142

143
    /// <summary>
144
    /// Creates an observable sequence that emits Windows messages for a message-only window with the specified class name.
145
    /// </summary>
146
    /// <remarks>The window is created on a dedicated background thread with a single-threaded apartment
147
    /// state. Disposal of the returned observable triggers destruction of the window and completion of the sequence.
148
    /// This method is useful for scenarios requiring low-level message processing without a visible UI window.</remarks>
149
    /// Can be null if no callback is needed.</param>
150
    /// <returns>An observable sequence of Windows messages received by the created message-only window. The sequence completes when the window is destroyed.</returns>
151
    private static IObservable<WindowMessage> CreateBaseStream()
152
    {
NEW
153
        return Observable.Create<WindowMessage>(observer =>
×
NEW
154
        {
×
NEW
155
            nint hwnd = 0;
×
NEW
156
            string className = "MsgLoop_" + Guid.NewGuid().ToString("N");
×
NEW
157
            WndProc? wndProcDelegate = null;
×
NEW
158

×
NEW
159
            var thread = new Thread(() =>
×
NEW
160
            {
×
NEW
161
                wndProcDelegate = (hWnd, msg, wParam, lParam) =>
×
NEW
162
                {
×
NEW
163
                    if (msg == (uint)WindowsMessages.WM_DESTROY)
×
NEW
164
                    {
×
NEW
165
                        PostQuitMessage(0);
×
NEW
166
                        return 0;
×
NEW
167
                    }
×
NEW
168

×
NEW
169
                    observer.OnNext(new WindowMessage(hWnd, msg, wParam, lParam));
×
NEW
170
                    return DefWindowProc(hWnd, msg, wParam, lParam);
×
NEW
171
                };
×
NEW
172

×
NEW
173
                // Register Class
×
NEW
174
                var wndClass = new WNDCLASSEX
×
NEW
175
                {
×
NEW
176
                    cbSize = (uint)Marshal.SizeOf<WNDCLASSEX>(),
×
NEW
177
                    lpfnWndProc = wndProcDelegate,
×
NEW
178
                    hInstance = GetModuleHandle(null),
×
NEW
179
                    lpszClassName = className
×
NEW
180
                };
×
NEW
181

×
NEW
182
                RegisterClassEx(ref wndClass);
×
NEW
183

×
NEW
184
                hwnd = CreateWindowEx(0, className, "NativeMessageWindow", 0, 0, 0, 0, 0, -3, 0, wndClass.hInstance, 0);
×
NEW
185

×
NEW
186
                // Trigger External Registrations
×
NEW
187
                if (hwnd != 0)
×
NEW
188
                {
×
NEW
189
                    _handleSubject.OnNext(hwnd);
×
NEW
190
                }
×
NEW
191

×
NEW
192
                while (GetMessage(out var msg, 0, 0, 0))
×
NEW
193
                {
×
NEW
194
                    TranslateMessage(ref msg);
×
NEW
195
                    DispatchMessage(ref msg);
×
NEW
196
                }
×
NEW
197

×
NEW
198
                // Trigger External Cleanup
×
NEW
199
                if (hwnd != 0)
×
NEW
200
                {
×
NEW
201
                    _handleSubject.OnNext(0);
×
NEW
202
                }
×
NEW
203
                
×
NEW
204
                UnregisterClass(className, wndClass.hInstance);
×
NEW
205
            });
×
NEW
206

×
NEW
207
            thread.SetApartmentState(ApartmentState.STA);
×
NEW
208
            thread.IsBackground = true;
×
NEW
209
            thread.Start();
×
NEW
210

×
NEW
211
            return Disposable.Create(() =>
×
NEW
212
            {
×
NEW
213
                if (hwnd != 0)
×
NEW
214
                {
×
NEW
215
                    PostMessage(hwnd, (uint)WindowsMessages.WM_DESTROY, 0, 0);
×
NEW
216
                }
×
NEW
217
            });
×
NEW
218
        });
×
219
    }
220
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc