Recent

Author Topic: [Solved] Get notified when monitors count/resolution changed  (Read 1293 times)

artem101

  • Jr. Member
  • **
  • Posts: 84
[Solved] Get notified when monitors count/resolution changed
« on: March 11, 2023, 12:09:28 pm »
I need to recieve notification every time when screen resolution changed, additional monitor connected/disconnected or any other screen configuration changes.

I implement this using X11 and its xrandr extension. This class simply catches any specified events in loop.

Code: Pascal  [Select][+][-]
  1. unit XRandREventWatcher;
  2.  
  3. {$mode ObjFPC}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, x, xlib, xrandr, ctypes;
  9.  
  10. type
  11.   TNotifyProc = procedure(const AEvent: TXEvent) of object;
  12.  
  13.   { TXRandREventWatcherThread }
  14.  
  15.   TXRandREventWatcherThread = class(TThread)
  16.   private
  17.     FNotifier: TNotifyProc;
  18.     FEventMask: cint;
  19.     FLastEvent: TXEvent;
  20.     procedure Notify;
  21.   protected
  22.     procedure Execute; override;
  23.   public
  24.     Constructor Create({ACreateSuspended: boolean;} AEventMask: cint;
  25.         ANotifier: TNotifyProc);
  26.   end;
  27.  
  28. implementation
  29.  
  30. { TXRandREventWatcherThread }
  31.  
  32. procedure TXRandREventWatcherThread.Notify;
  33. begin
  34.   if Assigned(FNotifier) then
  35.     FNotifier(FLastEvent);
  36. end;
  37.  
  38. procedure TXRandREventWatcherThread.Execute;
  39. var
  40.   //DisplayName: String;
  41.   Display: PDisplay;
  42.   RootWnd: TWindow;
  43. begin
  44.   //DisplayName := GetEnvironmentVariable('DISPLAY');
  45.   Display := XOpenDisplay({PChar(DisplayName)} Nil);
  46.   RootWnd := RootWindow(Display, DefaultScreen(Display));
  47.  
  48.   XRRSelectInput(Display, RootWnd, FEventMask);
  49.  
  50.   while True do
  51.   begin
  52.     if Terminated then
  53.       Break;
  54.  
  55.     XNextEvent(Display, @FLastEvent);
  56.  
  57.     Synchronize(@Notify);
  58.  
  59.     // FixMe: Freezes when no events and impossible to terminate
  60.   end;
  61.  
  62.   XCloseDisplay(Display);
  63. end;
  64.  
  65. constructor TXRandREventWatcherThread.Create(AEventMask: cint;
  66.   ANotifier: TNotifyProc);
  67. begin
  68.   FNotifier := ANotifier;
  69.   FEventMask := AEventMask;
  70.   FreeOnTerminate := {True} False;
  71.  
  72.   inherited Create(False);
  73. end;
  74.  
  75. end.

Usage in main form:

Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls,
  9.   xlib, xrandr, XRandREventWatcher;
  10.  
  11. type
  12.  
  13.   { TForm1 }
  14.  
  15.   TForm1 = class(TForm)
  16.     Memo1: TMemo;
  17.     procedure FormCreate(Sender: TObject);
  18.     procedure FormDestroy(Sender: TObject);
  19.   private
  20.     W: TXRandREventWatcherThread;
  21.  
  22.     procedure OnEvent(const AEvent: TXEvent);
  23.     procedure UpdateScreenInfo;
  24.   public
  25.  
  26.   end;
  27.  
  28. var
  29.   Form1: TForm1;
  30.  
  31. implementation
  32.  
  33. {$R *.lfm}
  34.  
  35. { TForm1 }
  36.  
  37. procedure TForm1.FormCreate(Sender: TObject);
  38. begin
  39.   UpdateScreenInfo;
  40.  
  41.   W := TXRandREventWatcherThread.Create(
  42.     RRScreenChangeNotifyMask, // Filter only screen configuration changed events
  43.     @onevent
  44.   );
  45. end;
  46.  
  47.  
  48. procedure TForm1.FormDestroy(Sender: TObject);
  49. begin
  50.   w.Terminate;
  51.   w.Free;
  52. end;
  53.  
  54. procedure TForm1.OnEvent(const AEvent: TXEvent);
  55.  
  56. begin
  57.   //Memo1.Append(Format('[%s] event #%d', [timetostr(now), AEvent._type]));
  58.   UpdateScreenInfo;
  59. end;
  60.  
  61. procedure TForm1.UpdateScreenInfo;
  62. var
  63.   I: Integer;
  64. begin
  65.   Screen.UpdateMonitors;
  66.  
  67.   Memo1.Clear;
  68.   Memo1.Append('Screen configuration changed on ' + TimeToStr(Now));
  69.   Memo1.Append('Monitors count: ' + IntToStr(Screen.MonitorCount));
  70.   for I := 0 to Screen.MonitorCount - 1 do
  71.   begin
  72.     Memo1.Append(Format('Monitor #%d %dx%d primary=%s', [
  73.         Screen.Monitors[I].MonitorNum,
  74.         Screen.Monitors[I].Width,
  75.         Screen.Monitors[I].Height,
  76.         BoolToStr(Screen.Monitors[I].Primary, 'Yes', 'No')
  77.     ]));
  78.   end;
  79. end;
  80.  
  81. end.

It works, but form freezes when I close it. Seems that loop in TXRandREventWatcherThread.Execute waits untill next event be recieved and only then will go to exit.

How to terminate it?
« Last Edit: April 04, 2023, 08:09:37 am by artem101 »

mikerabat

  • New Member
  • *
  • Posts: 39
Re: Get notified when monitors count/resolution changed
« Reply #1 on: March 27, 2023, 10:58:12 am »
First I'm from the win world so if I'm completely wrong don't jduge ;)
Here is what I would try:

The spec says that the XNextEvent actually blocks until an event arrives so
either create a SigTerminatefunction like this (pseudocode!):

procedure TXY.SigTerminate;
begin
        Terminate;
        XSendEvent( <params to be filled > );
end;

and in the thread change the logic a bit such that you check the terminated
flag after XNextEvent

In the main window signal the termination in the formDestroy and wait for the thread to terminate.



Another option is to not use the XNextEvent but rather check if there is an event in the first place -
maybe XIfEvent is an option??




artem101

  • Jr. Member
  • **
  • Posts: 84
Re: Get notified when monitors count/resolution changed
« Reply #2 on: March 28, 2023, 08:20:55 am »
        XSendEvent( <params to be filled > );

I don't understand well X11 system. What event I should send? Maybe there is any event which does nothing and can be catched with any event mask?
« Last Edit: March 28, 2023, 08:22:35 am by artem101 »

artem101

  • Jr. Member
  • **
  • Posts: 84
Re: Get notified when monitors count/resolution changed
« Reply #3 on: March 28, 2023, 09:49:13 am »
I add sending custom message, but still doesn't work.

Code: Pascal  [Select][+][-]
  1. unit XRandREventWatcher;
  2.  
  3. {$mode ObjFPC}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, x, xlib, xrandr, ctypes;
  9.  
  10. type
  11.   TNotifyProc = procedure(const AEvent: TXEvent) of object;
  12.  
  13.   { TXRandREventWatcherThread }
  14.  
  15.   TXRandREventWatcherThread = class(TThread)
  16.   private
  17.     FNotifier: TNotifyProc;
  18.     FEventMask: cint;
  19.     FLastEvent: TXEvent;
  20.     TerminateNotificationAtom: TAtom;
  21.     Display: PDisplay;
  22.     procedure Notify;
  23.   protected
  24.     procedure Execute; override;
  25.   public
  26.     Constructor Create({ACreateSuspended: boolean;} AEventMask: cint;
  27.         ANotifier: TNotifyProc);
  28.     destructor Destroy; override;
  29.  
  30.     procedure Terminate; //override;
  31.   end;
  32.  
  33. implementation
  34.  
  35. uses LazLoggerBase;
  36.  
  37. { TXRandREventWatcherThread }
  38.  
  39. procedure TXRandREventWatcherThread.Notify;
  40. begin
  41.   if Assigned(FNotifier) then
  42.     FNotifier(FLastEvent);
  43. end;
  44.  
  45. procedure TXRandREventWatcherThread.Execute;
  46. var
  47.   //DisplayName: String;
  48.   //Display: PDisplay;
  49.   RootWnd: TWindow;
  50. begin
  51.   writeln(ClassName, '::Execute() started');
  52.  
  53.   //DisplayName := GetEnvironmentVariable('DISPLAY');
  54.   //Display := XOpenDisplay({PChar(DisplayName)} Nil);
  55.   RootWnd := RootWindow(Display, DefaultScreen(Display));
  56.  
  57.   XRRSelectInput(Display, RootWnd, FEventMask {NoEventMask});
  58.  
  59.   while True do
  60.   begin
  61.     XNextEvent(Display, @FLastEvent);
  62.  
  63.     DebugLn('Recieved event: %d', [FLastEvent._type]);
  64.  
  65.     if Terminated then
  66.       Break;
  67.  
  68.     {if (FLastEvent._type = ClientMessage) and
  69.        (FLastEvent.message_type = TerminateNotificationAtom) then
  70.       Break;}
  71.  
  72.     Synchronize(@Notify);
  73.  
  74.     // FixMe: Freezes when no events and impossible to terminate
  75.   end;
  76.  
  77.   //XCloseDisplay(Display);
  78.  
  79.   writeln(ClassName, '::Execute() ended');
  80. end;
  81.  
  82. procedure TXRandREventWatcherThread.Terminate;
  83. var
  84.   XEvent: TXClientMessageEvent;
  85. begin
  86.   DebugLn(ClassName, '::Terminate() started');
  87.  
  88.   fillchar(XEvent,sizeof(XEvent),0);
  89.  
  90.   XEvent._type:=clientmessage;
  91.   //ev.send_event:=???;
  92.   //ev.serial:=???;
  93.   XEvent.display:={XOpenDisplay(Nil)}Display;
  94.   XEvent.window:=RootWindow(XEvent.display, DefaultScreen(XEvent.display));
  95.   XEvent.message_type:=TerminateNotificationAtom;
  96.   XEvent.format:=32;
  97.  
  98.  
  99.   XSendEvent(XEvent.display, XEvent.window, False, NoEventMask, @XEvent);
  100.  
  101.   inherited;
  102.  
  103.   DebugLn(ClassName, '::Terminate() ended');
  104. end;
  105.  
  106. constructor TXRandREventWatcherThread.Create(AEventMask: cint;
  107.   ANotifier: TNotifyProc);
  108. begin
  109.   FNotifier := ANotifier;
  110.   FEventMask := AEventMask;
  111.   FreeOnTerminate := {True} False;
  112.  
  113.   Display := XOpenDisplay({PChar(DisplayName)} Nil);
  114.  
  115.   TerminateNotificationAtom := XInternAtom(Display, 'XRandREventWatcher_Terminate_Notification', False);
  116.   DebugLn('TerminateNotificationAtom: %d', [TerminateNotificationAtom]);
  117.  
  118.   inherited Create(False);
  119.  
  120.   NameThreadForDebugging(ClassName, ThreadID);
  121.   DebugLn('Thread ', ClassName ,' started');
  122. end;
  123.  
  124. destructor TXRandREventWatcherThread.Destroy;
  125. begin
  126.   DebugLn(ClassName, '::Destroy() started');
  127.  
  128.   //Terminate;
  129.  
  130.   inherited Destroy;
  131.  
  132.   XCloseDisplay(Display);
  133.  
  134.   DebugLn(ClassName, '::Destroy() ended');
  135. end;
  136.  
  137. end.
  138.  
« Last Edit: March 28, 2023, 09:56:49 am by artem101 »

mikerabat

  • New Member
  • *
  • Posts: 39
Re: Get notified when monitors count/resolution changed
« Reply #4 on: March 28, 2023, 02:39:57 pm »
You should not name the function "Terminate" that does just introduce confusion...
Rather call it SigTerminate

SigTerminate first calls Terminat which actually sets the "Terminated" flag.
-> and then it's time to send a message to unlock the loop


Fred vS

  • Hero Member
  • *****
  • Posts: 3158
    • StrumPract is the musicians best friend
Re: Get notified when monitors count/resolution changed
« Reply #5 on: March 28, 2023, 04:17:22 pm »
Hello.

You may try with this:

Code: Pascal  [Select][+][-]
  1.     procedure TXRandREventWatcherThread.Execute;
  2.     var
  3.         RootWnd: TWindow;
  4.     begin
  5.       writeln(ClassName, '::Execute() started');
  6.      
  7.       RootWnd := RootWindow(Display, DefaultScreen(Display));
  8.      
  9.       XRRSelectInput(Display, RootWnd, FEventMask {NoEventMask});
  10.  
  11.      while True do
  12.         begin
  13.         if XPending(Display) > 0  then
  14.          begin
  15.            XNextEvent(Display, @FLastEvent);
  16.            DebugLn('Recieved event: %d', [FLastEvent._type]);  // event 89 is change of resolution
  17.            Synchronize(@Notify);
  18.          end;
  19.  
  20.       if Terminated then Break;
  21.        
  22.         sleep(100);
  23.  
  24.       end;
  25.  
  26.       writeln(ClassName, '::Execute() ended');
  27.     end;

And with this to destroy form:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.FormDestroy(Sender: TObject);
  2. begin
  3.   w.Terminate;
  4.   w.WaitFor;  // Add this
  5.   w.Free;
  6. end;
« Last Edit: March 28, 2023, 07:44:42 pm by Fred vS »
I use Lazarus 2.2.0 32/64 and FPC 3.2.2 32/64 on Debian 11 64 bit, Windows 10, Windows 7 32/64, Windows XP 32,  FreeBSD 64.
Widgetset: fpGUI, MSEgui, Win32, GTK2, Qt.

https://github.com/fredvs
https://gitlab.com/fredvs
https://codeberg.org/fredvs

MarkMLl

  • Hero Member
  • *****
  • Posts: 6676
Re: Get notified when monitors count/resolution changed
« Reply #6 on: March 28, 2023, 05:14:16 pm »
I broadly agree with Fred here, but would add a detail.

I few months ago I was looking at something- I think it was custom code for handling TCP messages but forget the detail- and found it was convenient to put a message handler in a thread to handle incoming messages and marshal them into a queue.

In order to terminate neatly I set up a single-thread HUP handler. That worked fine if the HUP was sent from the main thread, but not if it was sent from outside the program e.g. as a hint that a configuration file should be re-read. After much digging into documentation I learnt that that was more or less the expected behaviour.

So even if you are in a thread, if you have to monitor the Terminated flag you need to take steps not to block on anything else: find out how to poll, and/or make sure that you can use a timeout on any function if necessary by using fpselect().

And I don't know where that leaves you with low-level X11 events :-/

MarkMLl
MT+86 & Turbo Pascal v1 on CCP/M-86, multitasking with LAN & graphics in 128Kb.
Pet hate: people who boast about the size and sophistication of their computer.
GitHub repositories: https://github.com/MarkMLl?tab=repositories

artem101

  • Jr. Member
  • **
  • Posts: 84
Re: Get notified when monitors count/resolution changed
« Reply #7 on: March 28, 2023, 07:53:11 pm »
You may try with this:

It works! Thank you very much.

 

TinyPortal © 2005-2018