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

dapplo / Dapplo.Windows / 22253772036

21 Feb 2026 08:41AM UTC coverage: 32.897% (-0.5%) from 33.416%
22253772036

push

github

web-flow
Merge pull request #50 from dapplo/copilot/fix-clipboard-test-issue

Fix DragQueryFile P/Invoke charset causing garbled filenames from clipboard

618 of 2012 branches covered (30.72%)

Branch coverage included in aggregate %.

1692 of 5010 relevant lines covered (33.77%)

29.72 hits per line

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

63.16
/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.Native;
6
using Dapplo.Windows.Messages.Structs;
7
using System;
8
using System.Reactive.Disposables;
9
using System.Reactive.Linq;
10
using System.Reactive.Subjects;
11
using System.Runtime.InteropServices;
12
using System.Threading;
13

14
namespace Dapplo.Windows.Messages;
15

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

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

28
    [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
29
    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);
30

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

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

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

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

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

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

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

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

61
    private static IObservable<WindowMessage>? _sharedStream;
62
    private static readonly object _lock = new();
1✔
63
    private static readonly BehaviorSubject<nint> _handleSubject = new(0);
1✔
64

65
    /// <summary>
66
    /// Gets the current handle of the message window, or zero if no window is currently active.
67
    /// </summary>
68
    public static nint Handle => _handleSubject.Value;
25✔
69

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

93
    /// <summary>
94
    /// Subscribes to the shared message loop.
95
    /// </summary>
96
    /// <param name="onSetup">Invoked with the HWND when the window is created (or immediately if it already exists).</param>
97
    /// <param name="onTeardown">Invoked with the HWND when the window is destroyed OR when the subscription is disposed.</param>
98
    public static IObservable<WindowMessage> Listen(Action<nint> onSetup = null, Action<nint> onTeardown = null)
99
    {
100
        if (onSetup == null && onTeardown == null)
15!
101
        {
102
            return Messages;
15✔
103
        }
104

105
        return Observable.Create<WindowMessage>(observer =>
×
106
        {
×
107
            nint activeHwnd = 0;
×
108
            object handleLock = new();
×
109

×
110
            // Manage the Handle Lifecycle
×
111
            var handleSubscription = _handleSubject.Subscribe(hwnd =>
×
112
            {
×
113
                lock (handleLock)
×
114
                {
×
115
                    if (hwnd != 0)
×
116
                    {
×
117
                        activeHwnd = hwnd;
×
118
                        onSetup?.Invoke(activeHwnd);
×
119
                    }
×
120
                    else if (activeHwnd != 0)
×
121
                    {
×
122
                        // Window was destroyed
×
123
                        onTeardown?.Invoke(activeHwnd);
×
124
                        activeHwnd = 0;
×
125
                    }
×
126
                }
×
127
            });
×
128

×
129
            // Pass through the messages
×
130
            var messageSubscription = Messages.Subscribe(observer);
×
131

×
132
            // Ensure teardown runs if the consumer drops the subscription early
×
133
            return Disposable.Create(() =>
×
134
            {
×
135
                lock (handleLock)
×
136
                {
×
137
                    if (activeHwnd != 0)
×
138
                    {
×
139
                        onTeardown?.Invoke(activeHwnd);
×
140
                        activeHwnd = 0;
×
141
                    }
×
142
                }
×
143
                handleSubscription.Dispose();
×
144
                messageSubscription.Dispose();
×
145
            });
×
146
        });
×
147
    }
148

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

1✔
165
            var thread = new Thread(() =>
14✔
166
            {
14✔
167
                wndProcDelegate = (hWnd, msg, wParam, lParam) =>
14✔
168
                {
14✔
169
                    if (msg == (uint)WindowsMessages.WM_DESTROY)
71✔
170
                    {
14✔
171
                        PostQuitMessage(0);
14✔
172
                        return 0;
14✔
173
                    }
14✔
174

14✔
175
                    observer.OnNext(new WindowMessage(hWnd, msg, wParam, lParam));
57✔
176
                    return DefWindowProc(hWnd, msg, wParam, lParam);
57✔
177
                };
14✔
178

14✔
179
                // Register Class
14✔
180
                var wndClass = new WNDCLASSEX
14✔
181
                {
14✔
182
                    cbSize = (uint)Marshal.SizeOf<WNDCLASSEX>(),
14✔
183
                    lpfnWndProc = wndProcDelegate,
14✔
184
                    hInstance = GetModuleHandle(null),
14✔
185
                    lpszClassName = className
14✔
186
                };
14✔
187

14✔
188
                RegisterClassEx(ref wndClass);
14✔
189

14✔
190
                hwnd = CreateWindowEx(0, className, "NativeMessageWindow", 0, 0, 0, 0, 0, -3, 0, wndClass.hInstance, 0);
14✔
191

14✔
192
                // Trigger External Registrations
14✔
193
                if (hwnd != 0)
14✔
194
                {
14✔
195
                    _handleSubject.OnNext(hwnd);
14✔
196
                }
14✔
197

14✔
198
                while (GetMessage(out var msg, 0, 0, 0))
28✔
199
                {
14✔
200
                    TranslateMessage(ref msg);
14✔
201
                    DispatchMessage(ref msg);
14✔
202
                }
14✔
203

14✔
204
                // Trigger External Cleanup
14✔
205
                if (hwnd != 0)
14✔
206
                {
14✔
207
                    _handleSubject.OnNext(0);
14✔
208
                }
14✔
209
                
14✔
210
                UnregisterClass(className, wndClass.hInstance);
14✔
211
            });
28✔
212

1✔
213
            thread.SetApartmentState(ApartmentState.STA);
14✔
214
            thread.IsBackground = true;
14✔
215
            thread.Start();
14✔
216

1✔
217
            return Disposable.Create(() =>
14✔
218
            {
14✔
219
                if (hwnd != 0)
14✔
220
                {
14✔
221
                    PostMessage(hwnd, (uint)WindowsMessages.WM_DESTROY, 0, 0);
14✔
222
                }
14✔
223
            });
28✔
224
        });
1✔
225
    }
226
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc