• 0

[C#] How should I use ITaskbarList and how to detect program start?


Question

Hi, I cannot figure out and I also had no luck with google so I'm asking for your help...

I'd like to edit the taskbar buttons and I know (found in the VS Documentation) that I need the ITaskbarList interface but I don't no quite how to use such a unmanaged interface in C#... can you help?

The second question is: how can I detect that a program was launched (or started) in C#? I tried with a lot of WM_messages but I just can't catch this event... I also tested some hooks and so on, but it seems to catch only the program's own WM_CEATE message... how can I catch other programs' messages in my app?

The third question: how can I catch mouse move event outside of my app? As soon as the cursor leaves my app I receive no new messages from the system... there has to be some way to do it, like in SnagIt for example etc

7 answers to this question

Recommended Posts

  • 0

ok, so let's forget C# for a while... I'd like to know the concept how it works...

this is exacly what I've tried to do, I used the SetWindowsHookEx funtion and I hooked all the possible functions but they catch only messages of my own program... I suppose I use the wrong window handles, I mean this hwnd variables, but I can't find the right one

do you know what else do I need to be able to catch all messages? like moving the cursor outside of my app, or that some other program was started?

  • 0

You need to setup global hooks. This is the code I use to check for system events. Included are events for window creating / destroying, minimizing and foreground changes. It should give a good example if you need to include something else.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace Four13Designs
{
	public delegate void OnWindowMinimizeStartDelegate(IntPtr hWnd);
	public delegate void OnWindowMinimizeEndDelegate(IntPtr hWnd);
	public delegate void OnWindowForegroundChangedDelegate(IntPtr hWnd);
	public delegate void OnWindowDestroyDelegate(IntPtr hWnd);
	public delegate void OnWindowCreateDelegate(IntPtr hWnd);

	public sealed class Hooks
	{
		#region Windows API

		private enum SystemEvents : uint
		{
			EVENT_SYSTEM_CREATE = 3,
			EVENT_SYSTEM_DESTROY = 0x8001,
			EVENT_SYSTEM_MINIMIZESTART = 0x0016,
			EVENT_SYSTEM_MINIMIZEEND = 0x0017,
			EVENT_SYSTEM_FOREGROUND = 0x0003
		}

		private const uint WINEVENT_OUTOFCONTEXT = 0x0000;

		private delegate void WinEventDelegate(
			IntPtr hWinEventHook,
			uint eventType,
			IntPtr hWnd,
			int idObject,
			int idChild,
			uint dwEventThread,
			uint dwmsEventTime);

		[DllImport("User32.dll", SetLastError = true)]
		private static extern IntPtr SetWinEventHook(
			uint eventMin,
			uint eventMax,
			IntPtr hmodWinEventProc,
			WinEventDelegate lpfnWinEventProc,
			uint idProcess,
			uint idThread,
			uint dwFlags);

		[DllImport("user32.dll")]
		private static extern bool UnhookWinEvent(
			IntPtr hWinEventHook
			);

		#endregion

		private WinEventDelegate dEvent;
		private IntPtr pHook;
		public OnWindowMinimizeStartDelegate OnWindowMinimizeStart;
		public OnWindowMinimizeEndDelegate OnWindowMinimizeEnd;
		public OnWindowForegroundChangedDelegate OnWindowForegroundChanged;
		public OnWindowDestroyDelegate OnWindowDestroy;
		public OnWindowCreateDelegate OnWindowCreate;

		public Hooks()
		{
			dEvent = this.WinEvent;
			pHook = SetWinEventHook(
				(uint)SystemEvents.EVENT_SYSTEM_FOREGROUND,
				(uint)SystemEvents.EVENT_SYSTEM_DESTROY,
				IntPtr.Zero,
				dEvent,
				0,
				0,
				WINEVENT_OUTOFCONTEXT
				);

			if (IntPtr.Zero.Equals(pHook)) throw new Win32Exception();

			GC.KeepAlive(dEvent);
		}

		private void WinEvent(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
		{
			switch (eventType)
			{
				case (uint)SystemEvents.EVENT_SYSTEM_DESTROY:
					if (OnWindowDestroy != null) OnWindowDestroy(hWnd);
					break;
				case (uint)SystemEvents.EVENT_SYSTEM_FOREGROUND:
					if (OnWindowForegroundChanged != null) OnWindowForegroundChanged(hWnd);
					break;
				case (uint)SystemEvents.EVENT_SYSTEM_MINIMIZESTART:
					if (OnWindowMinimizeStart != null) OnWindowMinimizeStart(hWnd);
					break;
				case (uint)SystemEvents.EVENT_SYSTEM_MINIMIZEEND:
					if (OnWindowMinimizeEnd != null) OnWindowMinimizeEnd(hWnd);
					break;
				default:
					break;
			}
		}

		~Hooks()
		{
			if (!IntPtr.Zero.Equals(pHook)) UnhookWinEvent(pHook);
			pHook = IntPtr.Zero;
			dEvent = null;

			OnWindowCreate = null;
			OnWindowDestroy = null;
			OnWindowForegroundChanged = null;
			OnWindowMinimizeStart = null;
			OnWindowMinimizeEnd = null;
		}
	}
}

And this is the code I use for mouse / keyboard hooking:

using System;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using System.ComponentModel;

namespace gma.System.Windows
{
	/// <summary>
	/// This class allows you to tap keyboard and mouse and / or to detect their activity even when an 
	/// application runes in background or does not have any user interface at all. This class raises 
	/// common .NET events with KeyEventArgs and MouseEventArgs so you can easily retrive any information you need.
	/// </summary>
	public class UserActivityHook
	{
		#region Windows structure definitions

		/// <summary>
		/// The POINT structure defines the x- and y- coordinates of a point. 
		/// </summary>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/rectangl_0tiq.asp
		/// </remarks>
		[StructLayout(LayoutKind.Sequential)]
		private class POINT
		{
			/// <summary>
			/// Specifies the x-coordinate of the point. 
			/// </summary>
			public int x;
			/// <summary>
			/// Specifies the y-coordinate of the point. 
			/// </summary>
			public int y;
		}

		/// <summary>
		/// The MOUSEHOOKSTRUCT structure contains information about a mouse event passed to a WH_MOUSE hook procedure, MouseProc. 
		/// </summary>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookstructures/cwpstruct.asp
		/// </remarks>
		[StructLayout(LayoutKind.Sequential)]
		private class MouseHookStruct
		{
			/// <summary>
			/// Specifies a POINT structure that contains the x- and y-coordinates of the cursor, in screen coordinates. 
			/// </summary>
			public POINT pt;
			/// <summary>
			/// Handle to the window that will receive the mouse message corresponding to the mouse event. 
			/// </summary>
			public int hwnd;
			/// <summary>
			/// Specifies the hit-test value. For a list of hit-test values, see the description of the WM_NCHITTEST message. 
			/// </summary>
			public int wHitTestCode;
			/// <summary>
			/// Specifies extra information associated with the message. 
			/// </summary>
			public int dwExtraInfo;
		}

		/// <summary>
		/// The MSLLHOOKSTRUCT structure contains information about a low-level keyboard input event. 
		/// </summary>
		[StructLayout(LayoutKind.Sequential)]
		private class MouseLLHookStruct
		{
			/// <summary>
			/// Specifies a POINT structure that contains the x- and y-coordinates of the cursor, in screen coordinates. 
			/// </summary>
			public POINT pt;
			/// <summary>
			/// If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. 
			/// The low-order word is reserved. A positive value indicates that the wheel was rotated forward, 
			/// away from the user; a negative value indicates that the wheel was rotated backward, toward the user. 
			/// One wheel click is defined as WHEEL_DELTA, which is 120. 
			///If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP,
			/// or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, 
			/// and the low-order word is reserved. This value can be one or more of the following values. Otherwise, mouseData is not used. 
			///XBUTTON1
			///The first X button was pressed or released.
			///XBUTTON2
			///The second X button was pressed or released.
			/// </summary>
			public int mouseData;
			/// <summary>
			/// Specifies the event-injected flag. An application can use the following value to test the mouse flags. Value Purpose 
			///LLMHF_INJECTED Test the event-injected flag.  
			///0
			///Specifies whether the event was injected. The value is 1 if the event was injected; otherwise, it is 0.
			///1-15
			///Reserved.
			/// </summary>
			public int flags;
			/// <summary>
			/// Specifies the time stamp for this message.
			/// </summary>
			public int time;
			/// <summary>
			/// Specifies extra information associated with the message. 
			/// </summary>
			public int dwExtraInfo;
		}


		/// <summary>
		/// The KBDLLHOOKSTRUCT structure contains information about a low-level keyboard input event. 
		/// </summary>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookstructures/cwpstruct.asp
		/// </remarks>
		[StructLayout(LayoutKind.Sequential)]
		private class KeyboardHookStruct
		{
			/// <summary>
			/// Specifies a virtual-key code. The code must be a value in the range 1 to 254. 
			/// </summary>
			public int vkCode;
			/// <summary>
			/// Specifies a hardware scan code for the key. 
			/// </summary>
			public int scanCode;
			/// <summary>
			/// Specifies the extended-key flag, event-injected flag, context code, and transition-state flag.
			/// </summary>
			public int flags;
			/// <summary>
			/// Specifies the time stamp for this message.
			/// </summary>
			public int time;
			/// <summary>
			/// Specifies extra information associated with the message. 
			/// </summary>
			public int dwExtraInfo;
		}
		#endregion

		#region Windows function imports
		/// <summary>
		/// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain. 
		/// You would install a hook procedure to monitor the system for certain types of events. These events 
		/// are associated either with a specific thread or with all threads in the same desktop as the calling thread. 
		/// </summary>
		/// <param name="idHook">
		/// [in] Specifies the type of hook procedure to be installed. This parameter can be one of the following values.
		/// </param>
		/// <param name="lpfn">
		/// [in] Pointer to the hook procedure. If the dwThreadId parameter is zero or specifies the identifier of a 
		/// thread created by a different process, the lpfn parameter must point to a hook procedure in a dynamic-link 
		/// library (DLL). Otherwise, lpfn can point to a hook procedure in the code associated with the current process.
		/// </param>
		/// <param name="hMod">
		/// [in] Handle to the DLL containing the hook procedure pointed to by the lpfn parameter. 
		/// The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by 
		/// the current process and if the hook procedure is within the code associated with the current process. 
		/// </param>
		/// <param name="dwThreadId">
		/// [in] Specifies the identifier of the thread with which the hook procedure is to be associated. 
		/// If this parameter is zero, the hook procedure is associated with all existing threads running in the 
		/// same desktop as the calling thread. 
		/// </param>
		/// <returns>
		/// If the function succeeds, the return value is the handle to the hook procedure.
		/// If the function fails, the return value is NULL. To get extended error information, call GetLastError.
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/setwindowshookex.asp
		/// </remarks>
		[DllImport("user32.dll", CharSet = CharSet.Auto,
		   CallingConvention = CallingConvention.StdCall, SetLastError = true)]
		private static extern int SetWindowsHookEx(
			int idHook,
			HookProc lpfn,
			IntPtr hMod,
			int dwThreadId);

		/// <summary>
		/// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. 
		/// </summary>
		/// <param name="idHook">
		/// [in] Handle to the hook to be removed. This parameter is a hook handle obtained by a previous call to SetWindowsHookEx. 
		/// </param>
		/// <returns>
		/// If the function succeeds, the return value is nonzero.
		/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/setwindowshookex.asp
		/// </remarks>
		[DllImport("user32.dll", CharSet = CharSet.Auto,
			CallingConvention = CallingConvention.StdCall, SetLastError = true)]
		private static extern int UnhookWindowsHookEx(int idHook);

		/// <summary>
		/// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain. 
		/// A hook procedure can call this function either before or after processing the hook information. 
		/// </summary>
		/// <param name="idHook">Ignored.</param>
		/// <param name="nCode">
		/// [in] Specifies the hook code passed to the current hook procedure. 
		/// The next hook procedure uses this code to determine how to process the hook information.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies the wParam value passed to the current hook procedure. 
		/// The meaning of this parameter depends on the type of hook associated with the current hook chain. 
		/// </param>
		/// <param name="lParam">
		/// [in] Specifies the lParam value passed to the current hook procedure. 
		/// The meaning of this parameter depends on the type of hook associated with the current hook chain. 
		/// </param>
		/// <returns>
		/// This value is returned by the next hook procedure in the chain. 
		/// The current hook procedure must also return this value. The meaning of the return value depends on the hook type. 
		/// For more information, see the descriptions of the individual hook procedures.
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/setwindowshookex.asp
		/// </remarks>
		[DllImport("user32.dll", CharSet = CharSet.Auto,
			 CallingConvention = CallingConvention.StdCall)]
		private static extern int CallNextHookEx(
			int idHook,
			int nCode,
			int wParam,
			IntPtr lParam);

		/// <summary>
		/// The CallWndProc hook procedure is an application-defined or library-defined callback 
		/// function used with the SetWindowsHookEx function. The HOOKPROC type defines a pointer 
		/// to this callback function. CallWndProc is a placeholder for the application-defined 
		/// or library-defined function name.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/callwndproc.asp
		/// </remarks>
		private delegate int HookProc(int nCode, int wParam, IntPtr lParam);

		/// <summary>
		/// The ToAscii function translates the specified virtual-key code and keyboard 
		/// state to the corresponding character or characters. The function translates the code 
		/// using the input language and physical keyboard layout identified by the keyboard layout handle.
		/// </summary>
		/// <param name="uVirtKey">
		/// [in] Specifies the virtual-key code to be translated. 
		/// </param>
		/// <param name="uScanCode">
		/// [in] Specifies the hardware scan code of the key to be translated. 
		/// The high-order bit of this value is set if the key is up (not pressed). 
		/// </param>
		/// <param name="lpbKeyState">
		/// [in] Pointer to a 256-byte array that contains the current keyboard state. 
		/// Each element (byte) in the array contains the state of one key. 
		/// If the high-order bit of a byte is set, the key is down (pressed). 
		/// The low bit, if set, indicates that the key is toggled on. In this function, 
		/// only the toggle bit of the CAPS LOCK key is relevant. The toggle state 
		/// of the NUM LOCK and SCROLL LOCK keys is ignored.
		/// </param>
		/// <param name="lpwTransKey">
		/// [out] Pointer to the buffer that receives the translated character or characters. 
		/// </param>
		/// <param name="fuState">
		/// [in] Specifies whether a menu is active. This parameter must be 1 if a menu is active, or 0 otherwise. 
		/// </param>
		/// <returns>
		/// If the specified key is a dead key, the return value is negative. Otherwise, it is one of the following values. 
		/// Value Meaning 
		/// 0 The specified virtual key has no translation for the current state of the keyboard. 
		/// 1 One character was copied to the buffer. 
		/// 2 Two characters were copied to the buffer. This usually happens when a dead-key character 
		/// (accent or diacritic) stored in the keyboard layout cannot be composed with the specified 
		/// virtual key to form a single character. 
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/keyboardinput/keyboardinputreference/keyboardinputfunctions/toascii.asp
		/// </remarks>
		[DllImport("user32")]
		private static extern int ToAscii(
			int uVirtKey,
			int uScanCode,
			byte[] lpbKeyState,
			byte[] lpwTransKey,
			int fuState);

		/// <summary>
		/// The GetKeyboardState function copies the status of the 256 virtual keys to the 
		/// specified buffer. 
		/// </summary>
		/// <param name="pbKeyState">
		/// [in] Pointer to a 256-byte array that contains keyboard key states. 
		/// </param>
		/// <returns>
		/// If the function succeeds, the return value is nonzero.
		/// If the function fails, the return value is zero. To get extended error information, call GetLastError. 
		/// </returns>
		/// <remarks>
		/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/keyboardinput/keyboardinputreference/keyboardinputfunctions/toascii.asp
		/// </remarks>
		[DllImport("user32")]
		private static extern int GetKeyboardState(byte[] pbKeyState);

		[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
		private static extern short GetKeyState(int vKey);

		#endregion

		#region Windows constants

		//values from Winuser.h in Microsoft SDK.
		/// <summary>
		/// Windows NT/2000/XP: Installs a hook procedure that monitors low-level mouse input events.
		/// </summary>
		private const int WH_MOUSE_LL	   = 14;
		/// <summary>
		/// Windows NT/2000/XP: Installs a hook procedure that monitors low-level keyboard  input events.
		/// </summary>
		private const int WH_KEYBOARD_LL	= 13;

		/// <summary>
		/// Installs a hook procedure that monitors mouse messages. For more information, see the MouseProc hook procedure. 
		/// </summary>
		private const int WH_MOUSE		  = 7;
		/// <summary>
		/// Installs a hook procedure that monitors keystroke messages. For more information, see the KeyboardProc hook procedure. 
		/// </summary>
		private const int WH_KEYBOARD	   = 2;

		/// <summary>
		/// The WM_MOUSEMOVE message is posted to a window when the cursor moves. 
		/// </summary>
		private const int WM_MOUSEMOVE	  = 0x200;
		/// <summary>
		/// The WM_LBUTTONDOWN message is posted when the user presses the left mouse button 
		/// </summary>
		private const int WM_LBUTTONDOWN	= 0x201;
		/// <summary>
		/// The WM_RBUTTONDOWN message is posted when the user presses the right mouse button
		/// </summary>
		private const int WM_RBUTTONDOWN	= 0x204;
		/// <summary>
		/// The WM_MBUTTONDOWN message is posted when the user presses the middle mouse button 
		/// </summary>
		private const int WM_MBUTTONDOWN	= 0x207;
		/// <summary>
		/// The WM_LBUTTONUP message is posted when the user releases the left mouse button 
		/// </summary>
		private const int WM_LBUTTONUP	  = 0x202;
		/// <summary>
		/// The WM_RBUTTONUP message is posted when the user releases the right mouse button 
		/// </summary>
		private const int WM_RBUTTONUP	  = 0x205;
		/// <summary>
		/// The WM_MBUTTONUP message is posted when the user releases the middle mouse button 
		/// </summary>
		private const int WM_MBUTTONUP	  = 0x208;
		/// <summary>
		/// The WM_LBUTTONDBLCLK message is posted when the user double-clicks the left mouse button 
		/// </summary>
		private const int WM_LBUTTONDBLCLK  = 0x203;
		/// <summary>
		/// The WM_RBUTTONDBLCLK message is posted when the user double-clicks the right mouse button 
		/// </summary>
		private const int WM_RBUTTONDBLCLK  = 0x206;
		/// <summary>
		/// The WM_RBUTTONDOWN message is posted when the user presses the right mouse button 
		/// </summary>
		private const int WM_MBUTTONDBLCLK  = 0x209;
		/// <summary>
		/// The WM_MOUSEWHEEL message is posted when the user presses the mouse wheel. 
		/// </summary>
		private const int WM_MOUSEWHEEL	 = 0x020A;

		/// <summary>
		/// The WM_KEYDOWN message is posted to the window with the keyboard focus when a nonsystem 
		/// key is pressed. A nonsystem key is a key that is pressed when the ALT key is not pressed.
		/// </summary>
		private const int WM_KEYDOWN = 0x100;
		/// <summary>
		/// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem 
		/// key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, 
		/// or a keyboard key that is pressed when a window has the keyboard focus.
		/// </summary>
		private const int WM_KEYUP = 0x101;
		/// <summary>
		/// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when the user 
		/// presses the F10 key (which activates the menu bar) or holds down the ALT key and then 
		/// presses another key. It also occurs when no window currently has the keyboard focus; 
		/// in this case, the WM_SYSKEYDOWN message is sent to the active window. The window that 
		/// receives the message can distinguish between these two contexts by checking the context 
		/// code in the lParam parameter. 
		/// </summary>
		private const int WM_SYSKEYDOWN = 0x104;
		/// <summary>
		/// The WM_SYSKEYUP message is posted to the window with the keyboard focus when the user 
		/// releases a key that was pressed while the ALT key was held down. It also occurs when no 
		/// window currently has the keyboard focus; in this case, the WM_SYSKEYUP message is sent 
		/// to the active window. The window that receives the message can distinguish between 
		/// these two contexts by checking the context code in the lParam parameter. 
		/// </summary>
		private const int WM_SYSKEYUP = 0x105;

		private const byte VK_SHIFT	 = 0x10;
		private const byte VK_CAPITAL   = 0x14;
		private const byte VK_NUMLOCK   = 0x90;

		#endregion

		/// <summary>
		/// Creates an instance of UserActivityHook object and sets mouse and keyboard hooks.
		/// </summary>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		public UserActivityHook()
		{
			Start();
		}

		/// <summary>
		/// Creates an instance of UserActivityHook object and installs both or one of mouse and/or keyboard hooks and starts rasing events
		/// </summary>
		/// <param name="InstallMouseHook"><b>true</b> if mouse events must be monitored</param>
		/// <param name="InstallKeyboardHook"><b>true</b> if keyboard events must be monitored</param>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		/// <remarks>
		/// To create an instance without installing hooks call new UserActivityHook(false, false)
		/// </remarks>
		public UserActivityHook(bool InstallMouseHook, bool InstallKeyboardHook)
		{
			Start(InstallMouseHook, InstallKeyboardHook);
		}

		/// <summary>
		/// Destruction.
		/// </summary>
		~UserActivityHook()
		{
			//uninstall hooks and do not throw exceptions
			Stop(true, true, false);
		}

		/// <summary>
		/// Occurs when the user moves the mouse, presses any mouse button or scrolls the wheel
		/// </summary>
		public event MouseEventHandler OnMouseActivity;
		/// <summary>
		/// Occurs when the user presses a key
		/// </summary>
		public event KeyEventHandler KeyDown;
		/// <summary>
		/// Occurs when the user presses and releases 
		/// </summary>
		public event KeyPressEventHandler KeyPress;
		/// <summary>
		/// Occurs when the user releases a key
		/// </summary>
		public event KeyEventHandler KeyUp;


		/// <summary>
		/// Stores the handle to the mouse hook procedure.
		/// </summary>
		private int hMouseHook = 0;
		/// <summary>
		/// Stores the handle to the keyboard hook procedure.
		/// </summary>
		private int hKeyboardHook = 0;


		/// <summary>
		/// Declare MouseHookProcedure as HookProc type.
		/// </summary>
		private static HookProc MouseHookProcedure;
		/// <summary>
		/// Declare KeyboardHookProcedure as HookProc type.
		/// </summary>
		private static HookProc KeyboardHookProcedure;


		/// <summary>
		/// Installs both mouse and keyboard hooks and starts rasing events
		/// </summary>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		public void Start()
		{
			this.Start(true, true);
		}

		/// <summary>
		/// Installs both or one of mouse and/or keyboard hooks and starts rasing events
		/// </summary>
		/// <param name="InstallMouseHook"><b>true</b> if mouse events must be monitored</param>
		/// <param name="InstallKeyboardHook"><b>true</b> if keyboard events must be monitored</param>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		public void Start(bool InstallMouseHook, bool InstallKeyboardHook)
		{
			// install Mouse hook only if it is not installed and must be installed
			if (hMouseHook == 0 && InstallMouseHook)
			{
				// Create an instance of HookProc.
				MouseHookProcedure = new HookProc(MouseHookProc);
				//install hook
				hMouseHook = SetWindowsHookEx(
					WH_MOUSE_LL,
					MouseHookProcedure,
					Marshal.GetHINSTANCE(
						Assembly.GetExecutingAssembly().GetModules()[0]),
					0);
				//If SetWindowsHookEx fails.
				if (hMouseHook == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//do cleanup
					Stop(true, false, false);
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}

			// install Keyboard hook only if it is not installed and must be installed
			if (hKeyboardHook == 0 && InstallKeyboardHook)
			{
				// Create an instance of HookProc.
				KeyboardHookProcedure = new HookProc(KeyboardHookProc);
				//install hook
				hKeyboardHook = SetWindowsHookEx(
					WH_KEYBOARD_LL,
					KeyboardHookProcedure,
					Marshal.GetHINSTANCE(
					Assembly.GetExecutingAssembly().GetModules()[0]),
					0);
				//If SetWindowsHookEx fails.
				if (hKeyboardHook == 0)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//do cleanup
					Stop(false, true, false);
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}

		/// <summary>
		/// Stops monitoring both mouse and keyboard events and rasing events.
		/// </summary>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		public void Stop()
		{
			this.Stop(true, true, true);
		}

		/// <summary>
		/// Stops monitoring both or one of mouse and/or keyboard events and rasing events.
		/// </summary>
		/// <param name="UninstallMouseHook"><b>true</b> if mouse hook must be uninstalled</param>
		/// <param name="UninstallKeyboardHook"><b>true</b> if keyboard hook must be uninstalled</param>
		/// <param name="ThrowExceptions"><b>true</b> if exceptions which occured during uninstalling must be thrown</param>
		/// <exception cref="Win32Exception">Any windows problem.</exception>
		public void Stop(bool UninstallMouseHook, bool UninstallKeyboardHook, bool ThrowExceptions)
		{
			//if mouse hook set and must be uninstalled
			if (hMouseHook != 0 && UninstallMouseHook)
			{
				//uninstall hook
				int retMouse = UnhookWindowsHookEx(hMouseHook);
				//reset invalid handle
				hMouseHook = 0;
				//if failed and exception must be thrown
				if (retMouse == 0 && ThrowExceptions)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}

			//if keyboard hook set and must be uninstalled
			if (hKeyboardHook != 0 && UninstallKeyboardHook)
			{
				//uninstall hook
				int retKeyboard = UnhookWindowsHookEx(hKeyboardHook);
				//reset invalid handle
				hKeyboardHook = 0;
				//if failed and exception must be thrown
				if (retKeyboard == 0 && ThrowExceptions)
				{
					//Returns the error code returned by the last unmanaged function called using platform invoke that has the DllImportAttribute.SetLastError flag set. 
					int errorCode = Marshal.GetLastWin32Error();
					//Initializes and throws a new instance of the Win32Exception class with the specified error. 
					throw new Win32Exception(errorCode);
				}
			}
		}


		/// <summary>
		/// A callback function which will be called every time a mouse activity detected.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		private int MouseHookProc(int nCode, int wParam, IntPtr lParam)
		{
			// if ok and someone listens to our events
			if ((nCode >= 0) && (OnMouseActivity != null))
			{
				//Marshall the data from callback.
				MouseLLHookStruct mouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));

				//detect button clicked
				MouseButtons button = MouseButtons.None;
				short mouseDelta = 0;
				switch (wParam)
				{
					case WM_LBUTTONDOWN:
						//case WM_LBUTTONUP: 
						//case WM_LBUTTONDBLCLK: 
						button = MouseButtons.Left;
						break;
					case WM_RBUTTONDOWN:
						//case WM_RBUTTONUP: 
						//case WM_RBUTTONDBLCLK: 
						button = MouseButtons.Right;
						break;
					case WM_MOUSEWHEEL:
						//If the message is WM_MOUSEWHEEL, the high-order word of mouseData member is the wheel delta. 
						//One wheel click is defined as WHEEL_DELTA, which is 120. 
						//(value >> 16) & 0xffff; retrieves the high-order word from the given 32-bit value
						mouseDelta = (short)((mouseHookStruct.mouseData >> 16) & 0xffff);
						//TODO: X BUTTONS (I havent them so was unable to test)
						//If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, 
						//or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, 
						//and the low-order word is reserved. This value can be one or more of the following values. 
						//Otherwise, mouseData is not used. 
						break;
				}

				//double clicks
				int clickCount = 0;
				if (button != MouseButtons.None)
					if (wParam == WM_LBUTTONDBLCLK || wParam == WM_RBUTTONDBLCLK) clickCount = 2;
					else clickCount = 1;

				//generate event 
				 MouseEventArgs e = new MouseEventArgs(
													button,
													clickCount,
													mouseHookStruct.pt.x,
													mouseHookStruct.pt.y,
													mouseDelta);
				//raise it
				OnMouseActivity(this, e);
			}
			//call next hook
			return CallNextHookEx(hMouseHook, nCode, wParam, lParam);
		}

		/// <summary>
		/// A callback function which will be called every time a keyboard activity detected.
		/// </summary>
		/// <param name="nCode">
		/// [in] Specifies whether the hook procedure must process the message. 
		/// If nCode is HC_ACTION, the hook procedure must process the message. 
		/// If nCode is less than zero, the hook procedure must pass the message to the 
		/// CallNextHookEx function without further processing and must return the 
		/// value returned by CallNextHookEx.
		/// </param>
		/// <param name="wParam">
		/// [in] Specifies whether the message was sent by the current thread. 
		/// If the message was sent by the current thread, it is nonzero; otherwise, it is zero. 
		/// </param>
		/// <param name="lParam">
		/// [in] Pointer to a CWPSTRUCT structure that contains details about the message. 
		/// </param>
		/// <returns>
		/// If nCode is less than zero, the hook procedure must return the value returned by CallNextHookEx. 
		/// If nCode is greater than or equal to zero, it is highly recommended that you call CallNextHookEx 
		/// and return the value it returns; otherwise, other applications that have installed WH_CALLWNDPROC 
		/// hooks will not receive hook notifications and may behave incorrectly as a result. If the hook 
		/// procedure does not call CallNextHookEx, the return value should be zero. 
		/// </returns>
		private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam)
		{
			//indicates if any of underlaing events set e.Handled flag
			bool handled = false;
			//it was ok and someone listens to events
			if ((nCode >= 0) && (KeyDown != null || KeyUp != null || KeyPress != null))
			{
				//read structure KeyboardHookStruct at lParam
				KeyboardHookStruct MyKeyboardHookStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));
				//raise KeyDown
				if (KeyDown != null && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
				{
					Keys keyData = (Keys)MyKeyboardHookStruct.vkCode;
					KeyEventArgs e = new KeyEventArgs(keyData);
					KeyDown(this, e);
					handled = handled || e.Handled;
				}

				// raise KeyPress
				if (KeyPress != null && wParam == WM_KEYDOWN)
				{
					bool isDownShift = ((GetKeyState(VK_SHIFT) & 0x80) == 0x80 ? true : false);
					bool isDownCapslock = (GetKeyState(VK_CAPITAL) != 0 ? true : false);

					byte[] keyState = new byte[256];
					GetKeyboardState(keyState);
					byte[] inBuffer = new byte[2];
					if (ToAscii(MyKeyboardHookStruct.vkCode,
							  MyKeyboardHookStruct.scanCode,
							  keyState,
							  inBuffer,
							  MyKeyboardHookStruct.flags) == 1)
					{
						char key = (char)inBuffer[0];
						if ((isDownCapslock ^ isDownShift) && Char.IsLetter(key)) key = Char.ToUpper(key);
						KeyPressEventArgs e = new KeyPressEventArgs(key);
						KeyPress(this, e);
						handled = handled || e.Handled;
					}
				}

				// raise KeyUp
				if (KeyUp != null && (wParam == WM_KEYUP || wParam == WM_SYSKEYUP))
				{
					Keys keyData = (Keys)MyKeyboardHookStruct.vkCode;
					KeyEventArgs e = new KeyEventArgs(keyData);
					KeyUp(this, e);
					handled = handled || e.Handled;
				}

			}

			//if event handled in application do not handoff to other listeners
			if (handled)
				return 1;
			else
				return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
		}
	}
}

Just put those two into their own .cs files and call them from your program. From there, to use the system event hook add this to your programs code:

			!<Use this as a global reference

			private Hooks hooks = new Hooks();

			 !<This goes in your Form_Load method

			hooks.OnWindowCreate += new OnWindowCreateDelegate(window_Created);

			!<Now setup the window_Create method and add in the needed coding there. A handle will be passed to the window_Create method.

And to use the mouse hook add this to your program:

			!<Use this as a global reference

			private UserActivityHook actHook = new UserActivityHook(true, false);		   


			!<This goes in your Form_Load method

		   //Create a new event for the gloabl mouse hook
			actHook.OnMouseActivity += new MouseEventHandler(actHook_OnMouseActivity);
			this.actHook.Start(true, false);

Hope it helps. If you need any more help to get it working just let me know and I'll try my best to pass on the knowledge. :)

Edited by Sranshaft
  • 0

@ebody : I`ve also implemented something similar by using Global CBT Hooks to receive Window Create and Activation notifications on a system-wide basis for all windows, so will be happy to help :)

@ Sranshaft,

Excellent bit of code ! However , i have a couple of clarifications with Hook activities :

Basically, i`m working with a group of apps (really complex forms) running as separate exe`s and need to manage their ZOrder & Activation, so that the user gets the "feel" that they are interacting with one,single UI

All these windows are Unowned,Toplevel windows and have WS_EX-TOLLWINDOW style so that they dont show up either in the taskbar or the Alt+Tab menu

In the CBT hook filter functions i basically call SetWindowPos to keep all my app windows together.

You need to setup global hooks. This is the code I use to check for system events. Included are events for window creating / destroying, minimizing and foreground changes. It should give a good example if you need to include something else.

1. Should the Filter Functions for CBT Global Hooks always be in an unmanaged DLL ? I ask this because u have used C# in the entire sample but how can this get mapped globally into the address space of other unmanaged apps ?

2. Of the two approaches - SetWinEventHookEx or Global CBT Hooks : which one would u suggest is the better alternative? I`m aiming to get stability here and be informed about a wide range of events.

3. Third really important point is that i`m somehow messing up the activation states of the open windows.

Example scenario :

a. Right-click on any of my app windows and a context menu pops up,

b. Without selecting any item on the context menu, click on the taskbar icon of some external app like notepad

c. Click back on my earlier app , but now, the keyboard/mouse focus doesn`t seem to get transferred. Also my app window doesn`t seem to get activated even if i repeatedly click on it (left,right,middle) and i`m sure my hook also doesnt receive any activate notifications.

Is there any way to sync up the activation states ?

Recall that in my hook filter function, i check to see if any of my apps are activated (HCBT_ACTIVATE) and if yes then i change the zorder to bring my windows on top(HWND_TOP) else i send them to the Bottom.

I`m not really sure what more i`m supposed to do in my Filter functions. I tried calling SetActive / Foreground window but that doesn`t really help.

I`m getting the same behavior with SetWineventHook API too, so i guess my zordering code isn`t correct

I can send u a small sample if that helps.

Looking forward to your reply.Any pointers are most appreciated.

  • 0

the documentation is such a mess, I would never expect to find system event functions in the Accessibility group... that's why I couldn't find it earlier,... all the time I was looking for some system category or something hehe... unfortunetally I had no time to test it yet but I think it won't be harder the the window hook function that I tried to use for this purpose till now

This topic is now closed to further replies.
  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Posts

    • I gave the tool a chance the other day to make a USB. An hour later it was stuck at 0% downloaded. I downloaded the official ISO, downloaded Rufus, and made the USB myself in 15 min.
    • <Moved to software discussion and support> I've got fond memories of Winamp. Changing the skins, the different visualisations etc. But now I just need a simple music player. MSN messenger would be another one, MSN Messenger Plus (I think?) offered so many different plugins. But again, it probably wouldn't work for me these days. And then there is miRC. i think it's still going these days, but lord i had fun with that back in the day. Now it's mostly stuff like Discord, WhatsApp group chats, Signal, Telegram... /me is showing his age...
    • ive always been fascinated by old software this is an old video player for windows from apple
    • In the way that you framed it incorrectly. You wrote: "The constant need to close all browser sessions and wait for a new version to install" There's no "constant need to close all browser sessions". That's factually incorrect. The browser downloads its updates in the background and installs them when you open it again. Silently. And there's no "wait for a new version to install", updates are small and take 2-3 extra seconds AT MOST, if any. If you have an SSD, there's zero extra time. Also, every mainstream browser operates this way. Firefox, the FOSS go-to browser, the default on almost every Linux distro, does exactly the same. Also, you don't need to constantly restart Edge for updates to install, you can completely ignore them and it doesn't even ask you to handle them, it's all silent and automatic. So I don't understand what else do you want.
    • DuRoBo Krono Review: Portable E-Ink reader with great ideas that need a bit of improvement by Taras Buria Phone-sized e-readers are gaining traction these days, with more people treating them as a getaway device to cure phone addiction (or at least they are trying to) or having a more pocket-friendly reader that is easier to carry and hold. The market now has plenty of such readers to choose from, and DuRoBo is the latest addition, a new player that offers a more interesting approach to the idea. The Krono is a $279 e-reader with an interesting twist, which tries to make the device more fun and ergonomic. Here is my review. Disclaimer: DuRoBo provided the review sample without any editorial input or pre-approval. The Krono comes in a phone-sized box with pink accents. Inside, you get the device itself, a short user manual, and a USB cable. The cable is a bit old-fashioned, Type-A to Type-C, which is a bit disappointing. Hot take: I would rather have no cable in the box rather than another Type-A cable that gets immediately thrown into my box full of similar cables I never use. The Krono also has no charger in the box, as it relies on accessories you already own, which is fine with me. Here are the specs: Dimensions 154 x 80 x 9.0 mm or 6.06" x 3.15" x 0.35" 173 g or 6.10 oz Materials Black or White plastic Display 6.13-inch E-Ink Carta 1200, 1,648 x 824 pixels, 300 ppi Touch-capacitive. Dual-tone frontlight. Processor 8-core Qualcomm Snapdragon 690 (QTI SM6350) 2 performance cores at 2.07 GHz 4 efficiency cores at 1.71 GHz Memory 6 GB Storage 128GB, non-expandable ~104GB available out-of-the-box Operating system Android 15 with a custom launcher Connectivity Wi-Fi and Bluetooth Battery 3,950 mAh battery Buttons and port USB Type-C port Power button, Volume button, Smart Dial Breathing Lights Audio Mono Speaker and Dual microphones In the box The Krono, a Type-A to Type-C cable, user manual Price $279 on Amazon First impressions Right off the bat, no, this is not a phone replacement. Do not approach this device thinking it can serve you as a dumb phone to cure your TikTok addiction. In addition to the fact that the Krono has no cellular connectivity, I strongly believe that no amount of extra devices can fix your phone addiction until you put some serious effort into it. The Krono is a phone-sized e-reader, a companion for your phone dedicated to reading without distractions. The DuRoBo Krono is made of plastic with a very fine texture. It is hardly premium, but I also cannot say it feels cheap. The device is also a bit thick, quite dense, and well-built without rattling or cracking. You get to choose between two colors: white and black. The front has quite thick bezels, which is hardly surprising for an e-ink device. These things use front light, with LEDs usually placed on the screen perimeter. While I do not mind thicker bezels, the notably larger chin cheapens the look a little. What I mind is a notable seam between the display and the main case, which, after just two days of use, collected plenty of dust and specks. The back of the Krono is what makes the device stand out. There is a cylinder (DuRoBo calls it the Axis) embedded in the back of the reader, housing three elements: a power button on the right edge, a Smart Dial on the left edge, and "Breathing Lights" on the back. An etched DuRoBo logo sits below the cylinder, and it is the only piece of branding you can find on the device. Overall, the design and materials are very unassuming, but the cylinder with additional control elements certainly elevates the look and makes it more interesting. Other physical elements include two microphones (one on the top edge and one on the bottom edge), a USB Type-C port, a volume rocker, and a single mono speaker. There is no fingerprint reader, so if you want to protect your device, a PIN is your only option. The official TPU case is not the most premium-looking Display The Krono has a 6.1-inch E-Ink Carta 1200 touchscreen display with a resolution of 1,648 x 824 pixels (300 ppi). The display is front-lit, and you can adjust the brightness and temperature from cool to warm. Unfortunately, the Krono lacks automatic brightness and temperature adjustments, and you cannot set a custom schedule for the frontlight. However, you can set it to always enable frontlight so that you can see what is happening on the screen when turning it on in a dark environment. On the bright side (get it?), the front light can get extremely dim so that the screen is barely readable in a pitch-dark room. The front light is also uniform across the screen, with no noticeable temperature gradients. I am very susceptible to uneven front light, and it is very easy for me to notice it, but the Krono is doing a very good job in this area. I also like that the edge shadow is not very prominent and barely visible in the black variant. E-Ink Carta 1200 is not the newest generation (there are Carta 1250 and 1300), but it is still a good display. It supports three modes: Clarity, Speed, and Quality. In Clarity mode, text is very sharp and easy to read, but you trade that for more ghosting, a slower refresh rate, and more artifacts when the display changes images. Speed mode, as the name suggests, boosts refresh rate and reduces ghosting, but fine print and text become more jagged. Finally, Quality mode is only available in Android apps. It has the lowest refresh rate, but in return, you get much better visuals, improved gradients, and more. Like brightness and temperature, you can toggle modes from the control center. It is available when swiping from the top-right corner of the screen (the top-left is for notifications). I also like that the Krono can work as a desk clock when not in use. It has a bunch of screensavers, including horizontal clocks with time, date, and current battery level. The screen refreshes once per minute, and battery drain is extremely low (not even 1% in 24 hours). It is a great use of the technology, and another thing I wish more e-ink devices featured. Smart Dial The Smart Dial is Krono's main party trick. It sits on the left side of the device and serves multiple purposes. You can twist or press it to perform various actions, depending on the current use case scenario. When reading books, twisting the dial flips through pages, and pressing it refreshes the screen. On the home screen, the dial adjusts the brightness, and holding the dial pressed launches voice note recording. Finally, a quick double press launches the DuRoBo AI chatbot. While the dial scroll is not notched, it is very smooth and has haptic feedback that confirms your actions, which feels very nice. As a long-term Apple Watch user, I love the idea behind the dial. It feels very natural and oddly satisfying to use, especially with that subtle haptic feedback. I never liked flipping pages with touch input, and I strongly believe each e-reader should come with some sort of physical controls for turning pages. The Krono has both volume buttons (which also work as page turners) and the dial, so you are free to use whichever you prefer. With that said, the dial is not perfect. For one, it sticks out of the case way too far for my liking, raising concerns about durability and longevity when carrying the Krono around in a pocket (it is a pocket-sized device after all). Also, it has too much wobble, which cheapens the experience and makes it feel a bit flimsy and unsecured. While there are two plastic guards on the Krono's case, they are way too small for any kind of protection. I also think DuRoBo should let users customize dial actions (the only available customization is scroll direction), particularly for long and double presses. Not everyone needs voice notes, and DuRoBo AI does not work without an active internet connection, leaving the long press essentially useless when offline. I do not mind these features, and I genuinely think they are useful, but I would rather have the ability to toggle between screen modes, turn the frontlight on/off, or launch my favorite app. I also agree with people on Reddit asking developers to let users adjust the dial sensitivity. I hope this is something DuRoBo can implement with a software update to make the experience more personalized (it is a Smart Dial, after all) and incentivize users to fiddle with the Dial more often. The Dial is a fantastic idea, so please, guys, improve it a little. As for ergonomics, they are mostly fine, but the dial's position may feel a little awkward and way too high. When I use a phone or a phone-sized gadget, I tend to rest one of its corners on my palm for a more secure grip. With the Krono, such a grip is impossible because you cannot reach the dial even with big hands. You have to lower the reader a bit and hold it like a bottle without any extra support for the bottom edge. Such a grip is not necessarily uncomfortable (the Krono is also light enough for it), but it requires a bit of muscle retraining. Sometimes, I do not bother with the dial and hold the Krono like my phone, flipping through pages with volume buttons, as they are perfectly positioned for my right-hand thumb. Interestingly, when testing the Krono, I would often find myself thinking that a roller embedded in the long plastic cylinder on the back of the device would have been a much more comfortable solution. There is a free idea for you, guys. Software The Krono runs Android 15 with a very minimal launcher on top. The home screen presents you with a list of apps, a scrollable list of widgets, and your user profile. Widgets can display time, calendar, or recent books for quick access. You can also add or remove apps from the home screen to keep the most useful stuff around without tapping "Apps." I like this minimalistic approach; it looks clean, easy to understand, and light. I understand that some may find the list of all apps way too clean, but fortunately, DuRoBo lets you switch to traditional icons. The reader also has a bunch of preinstalled apps: Read: The default app for reading. Browser: A Chromium-based browser. Files: A simple file manager. Music: A simple music player. Spark: A voice recorder with transcription support and AI summarization DuRoBo AI: A built-in AI chatbot. Transfer: An app for file transfer over Wi-Fi. If that is not enough, there is the Google Play Store, where you can download all the extra apps you need, alternative readers, podcast apps, chatbots, and more. DuRoBo is not trying to give you an all-in-one device. The standard software experience is quite minimal, which makes it easy to approach and learn. The standard reader supports EPUB, EPUB3, AZW3, MOBI, PDF, TXT, DOC, and DOCX, which is more than enough to let you read most books without third-party software. As for customizing the reading experience, you can select one of five built-in fonts, adjust size and thickness, adjust margins and spacing (only three variants for each), change text alignment and direction, toggle the reading status bar, and switch to dark mode. There is also text-to-speech, which utilizes Android's default TTS tech. While I like the simplistic approach, I cannot help but feel DuRoBo could have made the built-in reader a bit more customizable. However, I am not going to bog down on this, as you can always install any other reader you prefer using the Play Store or by sideloading an APK. Getting books to the Krono is very simple. Given that the device is an Android smartphone without cellular connectivity, you can transfer files via a USB Type-C cable, download them using the built-in browser, share them over Bluetooth, or use cloud storage. My favorite was the built-in Transfer app. It is simple, reliable, and very well-designed. I was surprised by how well-designed the web portal is. It is fast, pretty, and properly categorized. Well done! Once you have your books loaded, you can highlight or underline text, add annotations, bookmark pages, check the table of contents, and ask AI about the selected text. Unfortunately, the Krono has no built-in vocabulary, but again, that is something a third-party reader could fix. Overall, the built-in reader is light and snappy, with just the minimum amount of features for a regular user to enjoy reading books. The Krono has no built-in reading tracking, so stat nerds will have to look for third-party reading apps. However, you can set a daily reading goal, and the reader will notify you when you reach it (for example, one hour). You can also set a reminder to read at a certain time, and when the time comes, the Krono will light up its back LEDs and unlock itself to nudge you. Other than that, the rear LEDs do nothing, not even showing charging progress, which is an unfortunate misopportunity if you ask me. Quirks aside, Krono's Android runs quite snappily and bug-free. Early reviews of the Krono criticized its Android 13-based software quite a lot, but now, the reader runs Android 15, and its software has fixed plenty of initial complaints. I never experienced any issues with built-in apps. AI attempts The DuRoBo Krono comes with a built-in AI chatbot. There is no information on what model powers this thing, but the system says it was "trained by Google." You can launch the bot from the app list or by double-pressing the dial. It works just like any other chatbot, and you can ask it anything by typing or using voice input. The AI saves your chats, and you can rename, export, or delete them. DuRoBo AI requires an active internet connection, and it does not work offline. Its reach and capabilities are also limited. You can only chat in the app and use it in the reader app as a makeshift vocabulary. However, the implementation is kinda awkward. You can only send a selected portion of text to AI without giving it any requests or instructions. I highlighted the word "dumb," and it apologized to me for not being useful. You also cannot ask follow-up questions or send the generated response to a separate chat. The chatbot is also slow, even with fast Wi-Fi, making the overall experience quite frustrating, which makes me again wish for the ability to remap the double press to something else. Spark, the standard voice recording app, also uses AI for note summarization and transcribing. Neither feature works offline, unfortunately. Spark records notes up to 30 minutes using Krono's dual microphones, and you can rename or export notes. Transcription quality is decent, and the speed is alright, but you can find much better solutions in the Google Play Store. What I like about Spark is that transcribed notes are not locked, and you can always type more to elaborate on your ideas, which is handy. Overall, I like that the Krono is not shoving AI down my throat, but to be honest, there is really not that much to shove. AI features here feel raw and need improvements to be more useful. Battery Life Like most E-Ink readers, the Krono has fantastic battery life. Even with a clock as a screensaver, its standby power consumption is incredibly low. And when in use, you can get weeks of reading on a single charge. Without the front light, my unit never sipped more than one or two percent of battery during a one-hour reading session. It was nice to see plenty of battery-related settings. You can limit charging at 80% to protect battery health long-term, check the number of charging cycles, manufacturing/first-time use date, battery health, and the maximum capacity. Additionally, the Krono lets you select what hardware remains enabled when sleeping. This lets you keep Wi-Fi and Bluetooth on (say, if you want to receive notifications, for some reason) and keep audio playing when locked. Turning these features off effectively eliminates any standby battery drain. I left my Krono sitting for 24 hours with a clock screensaver on, and it did not drop a single percent. The pretty big 3,950 mAh battery justifies the device's thickness and ensures you do not have to charge it for long periods. Speaking of charging, it is capped at only 10W, which is a bit disappointing, as getting such a big battery to 100% takes a notably long time in the era of super-fast charging smartphones. DuRoBo Moodi The Moodi is a standalone, optional accessory for your Krono. It is a wireless remote with two customizable buttons that you can use to flip pages, control media, or scroll webpages. The accessory connects via Bluetooth. Despite having a built-in rechargeable battery, it is extremely light. While the Moodi's shape and form factor is not what I would call particularly ergonomic, it is not uncomfortable to hold and use. The Moodi comes with six removable magnetic buttons with various smiley faces. Buttons sit securely, and they have nice-feeling, albeit a little loud, clicks. It is a cute touch that adds a little more fun and character to the device. There is also an accented power button and a single status LED. The latter displays charging status and connection mode. The Moodi supports three modes: Reading: Buttons work as volume buttons, allowing you to flip pages in the built-in reader or other apps that support page turning with volume buttons. Media: Buttons work as skip forward/backward, which is useful when listening to audiobooks, podcasts, or music. Scroll: The third mode lets you scroll pages in the web browser or any other application The Krono properly detects the Moodi and presents you with an on-screen guide when you connect it for the first time (it also displays the battery level). However, you can only change modes by holding both buttons for a few seconds. It is also worth noting that the Moodi works with other devices. I connected it to my iPhone and it let me adjust volume or control media playback. Sadly, the scroll did not work, so you cannot use it to waste time scrolling TikToks. Overall, the Moodi is a cute little accessory, which I can recommend for those who read a lot. It is very useful for remote page flipping when you do not want to burden your hands by holding the Krono all the time. I only wish DuRoBo included a lanyard for the built-in loop. As for the battery life, after using the Moodi for a few days, I only managed to drop several percent of its 90 mAh battery. Despite the small size, it is rated for weeks of use, which is pretty impressive. At $35.99, I cannot say the Moodi is a must-have accessory, but I see the appeal. I prefer using the Krono with its Smart Dial, as I rarely read for more than 40-60 minutes in one sitting. However, if you have a stand and like reading for long periods, the Moodi is the right thing to have. It is a bit more expensive than regular page flippers on Amazon, but it is on par with similar products from Kobo or BOOX. Plus, it has a little more fun to it with removable buttons and better integration into the Krono. Conclusion At the end of the day, DuRoBo Krono is a nice pocket-sized e-reader. Its software focuses on the main things without trying to be everything at once. The smart dial idea is unique and great, and I wish more manufacturers had something similar in their devices. The display is also good, with an even frontlight and "always-on" support. I did not notice any deal-breaking issues with the Krono. However, you can feel that the idea needs some improvements, such as a slightly stiffer dial in a more ergonomic location, perhaps a little more premium materials, and better software customization. I hope the company won't give up on the idea and improve the dial and ergonomics in the second generation. Buy DuRoBo Krono Black - $279.99 on Amazon Buy DuRoBo Krono White - $279.99 on Amazon Buy DuRoBo Moodi - $35.99 on Amazon As an Amazon Associate, we earn from qualifying purchases.
  • Recent Achievements

    • Conversation Starter
      flexorcist earned a badge
      Conversation Starter
    • One Month Later
      AndreaB earned a badge
      One Month Later
    • One Month Later
      agatameier earned a badge
      One Month Later
    • Week One Done
      agatameier earned a badge
      Week One Done
    • Week One Done
      ssd21345 earned a badge
      Week One Done
  • Popular Contributors

    1. 1
      +primortal
      518
    2. 2
      +Edouard
      195
    3. 3
      PsYcHoKiLLa
      147
    4. 4
      ATLien_0
      94
    5. 5
      Steven P.
      77
  • Tell a friend

    Love Neowin? Tell a friend!