diff --git a/samples/WinForm32/Form1.cs b/samples/WinForm32/Form1.cs index b8dd6fd..40017d7 100644 --- a/samples/WinForm32/Form1.cs +++ b/samples/WinForm32/Form1.cs @@ -18,6 +18,8 @@ namespace WinFormSample { public partial class Form1 : Form { + bool useDiyPump = true; + MessagePumpThread pump; TwainAppSession twain; readonly string saveFolder; readonly Stopwatch watch = new(); @@ -34,9 +36,10 @@ namespace WinFormSample var libVer = FileVersionInfo.GetVersionInfo(typeof(TwainAppSession).Assembly.Location).ProductVersion; Text += $"{(TWPlatform.Is32bit ? " 32bit" : " 64bit")} on NTwain {libVer}"; + pump = new MessagePumpThread(); TWPlatform.PreferLegacyDSM = false; - twain = new TwainAppSession(SynchronizationContext.Current!, Assembly.GetExecutingAssembly().Location); + twain = new TwainAppSession(Assembly.GetExecutingAssembly().Location); twain.StateChanged += Twain_StateChanged; twain.DefaultSourceChanged += Twain_DefaultSourceChanged; twain.CurrentSourceChanged += Twain_CurrentSourceChanged; @@ -62,11 +65,14 @@ namespace WinFormSample private void Twain_SourceDisabled(TwainAppSession sender, TW_IDENTITY_LEGACY e) { - if (watch.IsRunning) + BeginInvoke(() => { - watch.Stop(); - MessageBox.Show($"Took {watch.Elapsed} to finish that transfer."); - } + if (watch.IsRunning) + { + watch.Stop(); + MessageBox.Show($"Took {watch.Elapsed} to finish that transfer."); + } + }); } private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) @@ -85,18 +91,31 @@ namespace WinFormSample { base.OnHandleCreated(e); - - var hwnd = this.Handle; - var rc = twain.OpenDSM(hwnd); - twain.AddWinformFilter(); - Debug.WriteLine($"OpenDSM={rc}"); + if (useDiyPump) + { + _ = pump.AttachAsync(twain); + } + else + { + var hwnd = this.Handle; + var rc = twain.OpenDSM(hwnd, SynchronizationContext.Current!); + twain.AddWinformFilter(); + Debug.WriteLine($"OpenDSM={rc}"); + } } protected override void OnClosing(CancelEventArgs e) { - var finalState = twain.TryStepdown(STATE.S2); - Debug.WriteLine($"Stepdown result state={finalState}"); - twain.RemoveWinformFilter(); + if (useDiyPump) + { + pump.Detatch(); + } + else + { + var finalState = twain.TryStepdown(STATE.S2); + Debug.WriteLine($"Stepdown result state={finalState}"); + twain.RemoveWinformFilter(); + } base.OnClosing(e); } @@ -214,39 +233,42 @@ namespace WinFormSample private void Twain_DefaultSourceChanged(TwainAppSession sender, TW_IDENTITY_LEGACY ds) { - lblDefault.Text = ds.ProductName; + BeginInvoke(() => lblDefault.Text = ds.ProductName); } private void Twain_StateChanged(TwainAppSession sender, STATE state) { - Invoke(() => lblState.Text = state.ToString()); + BeginInvoke(() => lblState.Text = state.ToString()); } private void Twain_CurrentSourceChanged(TwainAppSession sender, TW_IDENTITY_LEGACY ds) { - lblCurrent.Text = ds.ToString(); - if (twain.State == STATE.S4) + BeginInvoke(() => { - LoadCapInfoList(); + lblCurrent.Text = ds.ToString(); + if (twain.State == STATE.S4) + { + LoadCapInfoList(); - // never seen a driver support these but here it is to test it - if (twain.GetCapLabel(CAP.ICAP_SUPPORTEDSIZES, out string? test).RC == TWRC.SUCCESS) - { - Debug.WriteLine($"Supported sizes label from ds = {test}"); + // never seen a driver support these but here it is to test it + if (twain.GetCapLabel(CAP.ICAP_SUPPORTEDSIZES, out string? test).RC == TWRC.SUCCESS) + { + Debug.WriteLine($"Supported sizes label from ds = {test}"); + } + if (twain.GetCapHelp(CAP.ICAP_SUPPORTEDSIZES, out string? test2).RC == TWRC.SUCCESS) + { + Debug.WriteLine($"Supported sizes help from ds = {test2}"); + } + if (twain.GetCapLabelEnum(CAP.ICAP_SUPPORTEDSIZES, out IList? test3).RC == TWRC.SUCCESS && test3 != null) + { + Debug.WriteLine($"Supported sizes label enum from ds = {string.Join(Environment.NewLine, test3)}"); + } } - if (twain.GetCapHelp(CAP.ICAP_SUPPORTEDSIZES, out string? test2).RC == TWRC.SUCCESS) + else { - Debug.WriteLine($"Supported sizes help from ds = {test2}"); + capListView.Items.Clear(); } - if (twain.GetCapLabelEnum(CAP.ICAP_SUPPORTEDSIZES, out IList? test3).RC == TWRC.SUCCESS && test3 != null) - { - Debug.WriteLine($"Supported sizes label enum from ds = {string.Join(Environment.NewLine, test3)}"); - } - } - else - { - capListView.Items.Clear(); - } + }); } private void LoadCapInfoList() diff --git a/samples/WinForm32/Program.cs b/samples/WinForm32/Program.cs index 85a61a9..e06b9af 100644 --- a/samples/WinForm32/Program.cs +++ b/samples/WinForm32/Program.cs @@ -16,14 +16,14 @@ namespace WinFormSample [STAThread] static void Main() { - if (DsmLoader.TryUseCustomDSM()) - { - Debug.WriteLine("Using our own dsm now :)"); - } - else - { - Debug.WriteLine("Will attempt to use default dsm :("); - } + //if (DsmLoader.TryUseCustomDSM()) + //{ + // Debug.WriteLine("Using our own dsm now :)"); + //} + //else + //{ + // Debug.WriteLine("Will attempt to use default dsm :("); + //} // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. diff --git a/samples/WinForm32/DsmLoader.cs b/src/NTwain/DSM/DsmLoader.cs similarity index 67% rename from samples/WinForm32/DsmLoader.cs rename to src/NTwain/DSM/DsmLoader.cs index 2359279..89c0567 100644 --- a/samples/WinForm32/DsmLoader.cs +++ b/src/NTwain/DSM/DsmLoader.cs @@ -1,6 +1,7 @@ using NTwain.Data; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -8,7 +9,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace WinFormSample +namespace NTwain.DSM { /// /// For demoing loading dsm from custom path in case @@ -19,15 +20,26 @@ namespace WinFormSample { static IntPtr __dllPtr; - public static bool TryUseCustomDSM() + public static bool TryLoadCustomDSM() { if (__dllPtr == IntPtr.Zero) { + var curFile = Assembly.GetExecutingAssembly().Location; + var dll = Path.Combine( - Path.GetDirectoryName(Environment.ProcessPath ?? Assembly.GetExecutingAssembly().Location)!, + Path.GetDirectoryName(curFile)!, $@"runtimes\win-{(TWPlatform.Is32bit ? "x86" : "x64")}\native\TWAINDSM.dll"); __dllPtr = LoadLibraryW(dll); + + if (__dllPtr != IntPtr.Zero) + { + Debug.WriteLine("Using our own dsm now :)"); + } + else + { + Debug.WriteLine("Will attempt to use default dsm :("); + } } return __dllPtr != IntPtr.Zero; } diff --git a/src/NTwain/Data/TWAINH_EXTRAS.cs b/src/NTwain/Data/TWAINH_EXTRAS.cs index e3c675a..eecdd7f 100644 --- a/src/NTwain/Data/TWAINH_EXTRAS.cs +++ b/src/NTwain/Data/TWAINH_EXTRAS.cs @@ -660,7 +660,7 @@ namespace NTwain.Data /// /// A simplified check on whether this has valid data from DSM. /// - public bool HasValue => Id == 0 && ProtocolMajor == 0 && ProtocolMinor == 0; + public bool HasValue => Id != 0 && ProtocolMajor != 0 && ProtocolMinor != 0; public override string ToString() { diff --git a/src/NTwain/MessagePumpThread.cs b/src/NTwain/MessagePumpThread.cs new file mode 100644 index 0000000..d74ea19 --- /dev/null +++ b/src/NTwain/MessagePumpThread.cs @@ -0,0 +1,100 @@ +#if WINDOWS || NETFRAMEWORK +using NTwain.Data; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace NTwain +{ + /// + /// For use under Windows to host a message pump in non-winform/wpf apps. + /// This is highly experimental. + /// + public class MessagePumpThread + { + DummyForm? _dummyForm; + TwainAppSession? _twain; + + public bool IsRunning => _dummyForm != null && _dummyForm.IsHandleCreated; + + /// + /// Starts the thread, attaches a twain session to it, + /// and opens the DSM. + /// + /// + /// + /// + public async Task AttachAsync(TwainAppSession twain) + { + if (twain.State > STATE.S2) throw new InvalidOperationException("Cannot attach to an opened TWAIN session."); + if (_twain != null || _dummyForm != null) throw new InvalidOperationException("Already attached previously."); + + Thread t = new(RunMessagePump); + t.IsBackground = true; + t.SetApartmentState(ApartmentState.STA); + t.Start(); + + while (_dummyForm == null || !_dummyForm.IsHandleCreated) + { + await Task.Delay(100); + } + + STS sts = default; + ManualResetEventSlim evt = new(); + _dummyForm.BeginInvoke(() => + { + sts = twain.OpenDSM(_dummyForm.Handle, SynchronizationContext.Current!); + if (sts.IsSuccess) + { + twain.AddWinformFilter(); + _twain = twain; + } + evt.Set(); + }); + evt.Wait(); + return sts; + } + + /// + /// Detatches a previously attached session and stops the thread. + /// + public void Detatch() + { + if (_dummyForm != null && _twain != null) + { + _dummyForm.BeginInvoke(_dummyForm.Close); + } + } + + void RunMessagePump() + { + Debug.WriteLine("TWAIN pump thread starting"); + _dummyForm = new DummyForm(); + _dummyForm.FormClosed += (s, e) => + { + if (_twain != null) + { + _twain.TryStepdown(STATE.S4); + _twain.RemoveWinformFilter(); + _twain.CloseDSM(); + _twain = null; + } + _dummyForm = null; + }; + Application.Run(_dummyForm); + Debug.WriteLine("TWAIN pump thread ended"); + } + + class DummyForm : Form + { + public DummyForm() + { + ShowInTaskbar = false; + WindowState = FormWindowState.Minimized; + } + } + } +} +#endif \ No newline at end of file diff --git a/src/NTwain/NTwain.csproj b/src/NTwain/NTwain.csproj index 69ca562..0dcfd99 100644 --- a/src/NTwain/NTwain.csproj +++ b/src/NTwain/NTwain.csproj @@ -21,6 +21,18 @@ + + + + + + + PreserveNewest + + + PreserveNewest + + diff --git a/src/NTwain/Native/WinNativeMethods.cs b/src/NTwain/Native/WinNativeMethods.cs index d18caf8..8242541 100644 --- a/src/NTwain/Native/WinNativeMethods.cs +++ b/src/NTwain/Native/WinNativeMethods.cs @@ -55,5 +55,43 @@ namespace NTwain.Native GPTR = GMEM_FIXED | GMEM_ZEROINIT, GHND = GMEM_MOVEABLE | GMEM_ZEROINIT } + +// [Flags] +// public enum PEEK_MESSAGE_REMOVE_TYPE : uint +// { +// PM_NOREMOVE = 0x00000000, +// PM_REMOVE = 0x00000001, +// PM_NOYIELD = 0x00000002, +// PM_QS_INPUT = 0x04070000, +// PM_QS_POSTMESSAGE = 0x00980000, +// PM_QS_PAINT = 0x00200000, +// PM_QS_SENDMESSAGE = 0x00400000, +// } + +//#if NET7_0_OR_GREATER +// [LibraryImport("USER32.dll")] +// public static partial int PeekMessageW(ref WIN_MESSAGE lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, PEEK_MESSAGE_REMOVE_TYPE wRemoveMsg); + +// [LibraryImport("USER32.dll", SetLastError = true)] +// public static partial int GetMessageW(ref WIN_MESSAGE lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + +// [LibraryImport("USER32.dll")] +// public static partial int TranslateMessage(ref WIN_MESSAGE lpMsg); + +// [LibraryImport("USER32.dll")] +// public static partial nint DispatchMessageW(ref WIN_MESSAGE lpMsg); +//#else +// [DllImport("USER32.dll")] +// public static extern int PeekMessageW(ref WIN_MESSAGE lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, PEEK_MESSAGE_REMOVE_TYPE wRemoveMsg); + +// [DllImport("USER32.dll", SetLastError = true)] +// public static extern int GetMessageW(ref WIN_MESSAGE lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + +// [DllImport("USER32.dll")] +// public static extern int TranslateMessage(ref WIN_MESSAGE lpMsg); + +// [DllImport("USER32.dll")] +// public static extern nint DispatchMessageW(ref WIN_MESSAGE lpMsg); +//#endif } } diff --git a/src/NTwain/TwainAppSession.Callbacks.cs b/src/NTwain/TwainAppSession.Callbacks.cs index bb93bcb..bd6c8f4 100644 --- a/src/NTwain/TwainAppSession.Callbacks.cs +++ b/src/NTwain/TwainAppSession.Callbacks.cs @@ -112,24 +112,17 @@ namespace NTwain if (!_inTransfer) { // this should be done on ui thread (or same one that enabled the ds) - _uiThreadMarshaller.Post(obj => - { - ((TwainAppSession)obj!).DisableSource(); - }, this); + DisableSource(); } break; case MSG.DEVICEEVENT: if (DeviceEvent != null && DGControl.DeviceEvent.Get(ref _appIdentity, ref _currentDS, out TW_DEVICEEVENT de) == TWRC.SUCCESS) { - _uiThreadMarshaller.Post(obj => + try { - try - { - var twain = (TwainAppSession)obj!; - twain.DeviceEvent!.Invoke(twain, de); - } - catch { } - }, this); + DeviceEvent.Invoke(this, de); + } + catch { } } break; } diff --git a/src/NTwain/TwainAppSession.PropEvents.cs b/src/NTwain/TwainAppSession.PropEvents.cs index 02f6138..bd7eb1f 100644 --- a/src/NTwain/TwainAppSession.PropEvents.cs +++ b/src/NTwain/TwainAppSession.PropEvents.cs @@ -57,14 +57,11 @@ namespace NTwain if (_state != value) { _state = value; - _uiThreadMarshaller.Send(obj => + try { - try - { - ((TwainAppSession)obj!).StateChanged?.Invoke(this, value); - } - catch { } - }, this); + StateChanged?.Invoke(this, value); + } + catch { } } } } diff --git a/src/NTwain/TwainAppSession.Sources.cs b/src/NTwain/TwainAppSession.Sources.cs index 885caf1..0b6c277 100644 --- a/src/NTwain/TwainAppSession.Sources.cs +++ b/src/NTwain/TwainAppSession.Sources.cs @@ -105,6 +105,18 @@ namespace NTwain /// Otherwise capturing will begin after this. /// public STS EnableSource(bool showUI, bool uiOnly) + { + if (_pumpThreadMarshaller == null) return EnableSourceReal(showUI, uiOnly); + + var sts = default(STS); + _pumpThreadMarshaller.Send(_ => + { + sts = EnableSourceReal(showUI, uiOnly); + }, null); + return sts; + } + + private STS EnableSourceReal(bool showUI, bool uiOnly) { if (State > STATE.S4) { @@ -136,6 +148,17 @@ namespace NTwain /// /// public STS DisableSource() + { + if (_pumpThreadMarshaller == null) return DisableSourceReal(); + + var sts = default(STS); + _pumpThreadMarshaller.Send(_ => + { + sts = DisableSourceReal(); + }, null); + return sts; + } + STS DisableSourceReal() { _closeDsRequested = true; var rc = DGControl.UserInterface.DisableDS(ref _appIdentity, ref _currentDS, ref _userInterface); diff --git a/src/NTwain/TwainAppSession.Xfers.cs b/src/NTwain/TwainAppSession.Xfers.cs index a8fe599..c09bf8b 100644 --- a/src/NTwain/TwainAppSession.Xfers.cs +++ b/src/NTwain/TwainAppSession.Xfers.cs @@ -67,18 +67,11 @@ namespace NTwain do { var readyArgs = new TransferReadyEventArgs(pending.Count, (TWEJ)pending.EOJ); - if (TransferReady != null) + try { - _uiThreadMarshaller.Send(_ => - { - try - { - - TransferReady.Invoke(this, readyArgs); - } - catch { } // don't let consumer kill the loop if they have exception - }, null); + TransferReady?.Invoke(this, readyArgs); } + catch { } // don't let consumer kill the loop if they have exception if (readyArgs.Cancel == CancelType.EndNow || _closeDsRequested) { @@ -150,27 +143,21 @@ namespace NTwain } catch (Exception ex) { - _uiThreadMarshaller.Send(obj => + try { - try - { - var twain = ((TwainAppSession)obj!); - twain.TransferError?.Invoke(twain, new TransferErrorEventArgs(ex)); - } - catch { } - }, this); + TransferError?.Invoke(this, new TransferErrorEventArgs(ex)); + } + catch { } } } } while (sts.RC == TWRC.SUCCESS && pending.Count != 0); } HandleXferCode(ref sts, ref pending); + if (State >= STATE.S5) { - _uiThreadMarshaller.Send(obj => - { - ((TwainAppSession)obj!).DisableSource(); - }, this); + DisableSource(); } _inTransfer = false; } @@ -185,11 +172,11 @@ namespace NTwain break; case TWRC.CANCEL: // might eventually have option to cancel this or all like transfer ready - _uiThreadMarshaller.Send(obj => + try { - var twain = ((TwainAppSession)obj!); - twain.TransferCanceled?.Invoke(twain, new TransferCanceledEventArgs()); - }, this); + TransferCanceled?.Invoke(this, new TransferCanceledEventArgs()); + } + catch { } sts = WrapInSTS(DGControl.PendingXfers.EndXfer(ref _appIdentity, ref _currentDS, ref pending)); sts = WrapInSTS(DGControl.PendingXfers.Reset(ref _appIdentity, ref _currentDS, ref pending)); if (sts.RC == TWRC.SUCCESS) State = STATE.S5; diff --git a/src/NTwain/TwainAppSession.cs b/src/NTwain/TwainAppSession.cs index 39c140c..b360a98 100644 --- a/src/NTwain/TwainAppSession.cs +++ b/src/NTwain/TwainAppSession.cs @@ -19,26 +19,24 @@ namespace NTwain /// /// Creates TWAIN session with app info derived an executable file. /// - /// /// /// /// - public TwainAppSession(SynchronizationContext uiThreadMarshaller, + public TwainAppSession( string exeFilePath, TWLG appLanguage = TWLG.ENGLISH_USA, TWCY appCountry = TWCY.USA) : - this(uiThreadMarshaller, FileVersionInfo.GetVersionInfo(exeFilePath), appLanguage, appCountry) + this(FileVersionInfo.GetVersionInfo(exeFilePath), appLanguage, appCountry) { } /// /// Creates TWAIN session with app info derived from a object. /// - /// /// /// /// - public TwainAppSession(SynchronizationContext uiThreadMarshaller, + public TwainAppSession( FileVersionInfo appInfo, TWLG appLanguage = TWLG.ENGLISH_USA, TWCY appCountry = TWCY.USA) : - this(uiThreadMarshaller, + this( appInfo.CompanyName ?? "", appInfo.ProductName ?? "", appInfo.ProductName ?? "", @@ -48,7 +46,6 @@ namespace NTwain /// /// Creates TWAIN session with explicit app info. /// - /// /// /// /// @@ -57,12 +54,15 @@ namespace NTwain /// /// /// - public TwainAppSession(SynchronizationContext uiThreadMarshaller, + public TwainAppSession( string companyName, string productFamily, string productName, Version productVersion, string productDescription = "", TWLG appLanguage = TWLG.ENGLISH_USA, TWCY appCountry = TWCY.USA, DG supportedTypes = DG.IMAGE) { +#if WINDOWS || NETFRAMEWORK + DSM.DsmLoader.TryLoadCustomDSM(); +#endif // todo: find a better place for this if (!__encodingRegistered) { @@ -93,7 +93,6 @@ namespace NTwain _legacyCallbackDelegate = LegacyCallbackHandler; _osxCallbackDelegate = OSXCallbackHandler; - _uiThreadMarshaller = uiThreadMarshaller; StartTransferThread(); } @@ -104,7 +103,7 @@ namespace NTwain #endif // test threads a bit //readonly BlockingCollection _bgPendingMsgs = new(); - private readonly SynchronizationContext _uiThreadMarshaller; + SynchronizationContext? _pumpThreadMarshaller; bool _closeDsRequested; bool _inTransfer; readonly AutoResetEvent _xferReady = new(false); @@ -176,13 +175,15 @@ namespace NTwain /// Loads and opens the TWAIN data source manager. /// /// Required if on Windows. + /// Context for TWAIN to invoke certain actions on the thread that the hwnd lives on. /// - public STS OpenDSM(IntPtr hwnd) + public STS OpenDSM(IntPtr hwnd, SynchronizationContext uiThreadMarshaller) { var rc = DGControl.Parent.OpenDSM(ref _appIdentity, hwnd); if (rc == TWRC.SUCCESS) { _hwnd = hwnd; + _pumpThreadMarshaller = uiThreadMarshaller; State = STATE.S3; // get default source if (DGControl.Identity.GetDefault(ref _appIdentity, out TW_IDENTITY_LEGACY ds) == TWRC.SUCCESS) @@ -223,6 +224,7 @@ namespace NTwain } catch { } _hwnd = IntPtr.Zero; + _pumpThreadMarshaller = null; } return WrapInSTS(rc, true); } diff --git a/src/NTwain/runtimes/win-x64/native/TWAINDSM.dll b/src/NTwain/runtimes/win-x64/native/TWAINDSM.dll new file mode 100644 index 0000000..2b981aa Binary files /dev/null and b/src/NTwain/runtimes/win-x64/native/TWAINDSM.dll differ diff --git a/src/NTwain/runtimes/win-x86/native/TWAINDSM.dll b/src/NTwain/runtimes/win-x86/native/TWAINDSM.dll new file mode 100644 index 0000000..7f38f2f Binary files /dev/null and b/src/NTwain/runtimes/win-x86/native/TWAINDSM.dll differ