Ooops
September 23, 2021, 18:11:23

Author Topic: How to output different MaxGUI windows to different devices?  (Read 756 times)

Offline chalky

  • Jr. Member
  • **
  • Posts: 87
I have been asked by an exam-invigilator friend to write a program which displays a clock (and exam-module information) during examinations. I have successfuly created this using a bog-standard BlitzMax window, and just need to re-write it to use a MaxGUI canvas so that it can be displayed in a borderless window and scaled to fit the target display.



However, while the clock is being displayed 'fullscreen' to the candidates on one device (i.e. digital overhead projector), a 'control' window needs to be displayed on the invigilator's PC so they can configure the exam session and start (and stop if necessary due to a fire-alarm etc.) the clock/timer.

Is it possible to do this via BlitzMax/MaxGUI, and if so can anyone (I'm not looking for a complete solution) point me in the right direction?
« Last Edit: June 04, 2021, 19:09:09 by chalky »

Offline Henri

  • Sr. Member
  • ****
  • Posts: 353
Re: How to output different MaxGUI windows to different devices?
« Reply #1 on: June 04, 2021, 22:49:32 »
Hi,

I don't see why not. I assume the projector is connected to your friends computer, then its just a matter of extending the desktop. You can create dual windows where one is the controller with the buttons and other is the displayed clock. In my mind the biggest hurdle (aside from drawing a clock programmatically) is to find out the projectors display coordinates and dimensions (aka the second screen). If you can figure those, you can display the other window at that location and size.

I would probably start experimenting with the canvas example provided in the bmax docs folder (png-file needed, I modified it a bit).

Code: BlitzMax
  1. ' redrawgadget.bmx
  2.  
  3. ' version 3 - fixed to be compatible with virtual resolutions
  4.  
  5. Import MaxGui.Drivers
  6.  
  7. AutoMidHandle True
  8.  
  9. Local spinner:TSpinningApplet = New TSpinningApplet.Create()
  10.  
  11. Repeat
  12.         WaitSystem()
  13. Forever
  14.  
  15. Type TApplet
  16.  
  17.         Method OnEvent(Event:TEvent) Abstract
  18.  
  19.         Method Run()
  20.                 AddHook EmitEventHook,eventhook,Self
  21.         End Method
  22.  
  23.         Function eventhook:Object(id,data:Object,context:Object)
  24.                 Local event:TEvent = TEvent(data)
  25.                 Local app:TApplet = TApplet(context)
  26.                 app.OnEvent( event )
  27.                 Return data
  28.         End Function
  29.  
  30. End Type
  31.  
  32. Type TSpinningApplet Extends TApplet
  33.        
  34.         Global image:TImage
  35.        
  36.         Field timer:TTimer, state:Int
  37.         Field window:TGadget, canvas:TGadget
  38.         Field control:TGadget, button:Tgadget
  39.        
  40.         Method Draw()
  41.                
  42.                 SetGraphics CanvasGraphics(canvas)
  43.                 SetVirtualResolution ClientWidth(canvas),ClientHeight(canvas)
  44.                 SetViewport 0,0,ClientWidth(canvas), ClientHeight(canvas)
  45.                
  46.                 SetBlend( ALPHABLEND )
  47.                 SetRotation( MilliSecs()*.1 )
  48.                 SetClsColor( 255, 0, 0 )
  49.                
  50.                 Cls()
  51.                 DrawImage( image, GraphicsWidth()/2, GraphicsHeight()/2 )
  52.                
  53.                 Flip()
  54.                
  55.         End Method
  56.        
  57.         Method OnEvent(event:TEvent)
  58.                 If Not event Then Return
  59.                 Select event.id
  60.                         Case EVENT_WINDOWCLOSE, EVENT_APPTERMINATE
  61.                                 End
  62.                         Case EVENT_TIMERTICK
  63.                                 RedrawGadget( canvas )
  64.                         Case EVENT_GADGETPAINT
  65.                                 If (event.source = canvas) Then Draw()
  66.                         Case EVENT_GADGETACTION
  67.                                
  68.                                 If Not state Then
  69.                                         timer.Stop()
  70.                                         state = True
  71.                                 Else
  72.                                         timer = CreateTimer( 100 )
  73.                                         state = False
  74.                                 EndIf
  75.                                
  76.                 End Select
  77.         End Method
  78.        
  79.         Method Create:TSpinningApplet()
  80.                
  81.                 If Not image Then image = LoadImage( "fltkwindow.png" )
  82.                 If Not image Then Notify("Could not load image"); Return
  83.                
  84.                 window = CreateWindow( "Clock", 20, 20, 512, 512, Null, WINDOW_CENTER )
  85.                 control = CreateWindow( "Control", 20, 20, 300, 200 )
  86.                 button = CreateButton("Start/Stop", 20, 20, 60, 30, control)
  87.                 Local w = ClientWidth(window)
  88.                 Local h = ClientHeight(window)
  89.                
  90.                 canvas = CreateCanvas( 0, 0, w, h, window )
  91.                 SetGadgetLayout( canvas, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED )
  92.                
  93.                 timer = CreateTimer( 100 )
  94.                 Run()
  95.                
  96.                 Return Self
  97.                
  98.         End Method
  99.        
  100. End Type
  101.  


-Henri
- Got 01100011 problems, but the bit ain't 00000001

Offline col

  • Hero Member
  • *****
  • Posts: 592
Re: How to output different MaxGUI windows to different devices?
« Reply #2 on: June 05, 2021, 04:42:41 »
Hiya, I've done exactly this in Windows (using c++). Actually I did more that in that I have a panel type window on the main display and duplicate it to be fullscreen on the 2nd display but yeah your problem can be done resolved.

When I get some free time I'll post the APIs and how I did it. It may or may not help as your want to use BMax GUI but I'll post it later anyway... you never know.

Offline chalky

  • Jr. Member
  • **
  • Posts: 87
Re: How to output different MaxGUI windows to different devices?
« Reply #3 on: June 05, 2021, 15:06:00 »
@Henri
Thankfully I've already done the clock - the pic in my first post is a screenshot of my program running. Thank you for taking the time to post a code example - I'm actually ok implimenting MaxGUI canvases and have now successfully converted all the drawing operations to use one inside a borderless window (which automatically scales everything when required to fit fullscreen). The problem is that DeskTopWidth(), DeskTopHeight(), GadgetWidth(Desktop()) & GadgetHeight(Desktop()) only return the resolution of the primary display (my multi-monitor setup [2560x1440 & 1920x1080] returns 2560 and 1440). Without being able to determine the resolution of a 2nd monitor (or projector etc.) I cannot calculate the scaling-factor.

@col
That would be really useful - thank you. Even if ultimately I cannot use it I will definitely learn something by looking at your code.
« Last Edit: June 05, 2021, 15:20:43 by chalky »

Offline col

  • Hero Member
  • *****
  • Posts: 592
Re: How to output different MaxGUI windows to different devices?
« Reply #4 on: June 06, 2021, 04:47:17 »
In this code it makes some assumptions due to the strict nature of the environment it is used in:

1. The second monitor will show a full screen duplicate of a smaller section of the main application window.
2. The 'main' monitor (and therefore the main window) is always on the left with coordinates starting at 0,0. The main window (borderless) has its coordinates hard coded started at 0,0 and is fullscreen for monitor 1.
3. Therefore a 2nd monitor will never have its coordinates at 0,0. In Windows this can be changed - but as we have full control of the end unit, the user cannot change this (it is used in 'kiosk' type of environment).
4. There are only ever a maximum of 2 monitors attached. Again a hardware restriction we impose due to the environment it is used in.
5. The code handles the situation where you can plug-in and un-plug the 2nd monitor while the app is running. If you don't handle this then Windows will move any windows on the 2nd monitor at the time of unplugging on to the 1st monitor - when the code detects that the monitor has been either unplugged and plugged in it will show or hide the full screen window. This saves having to tear down and recreate that 2nd full screen window.

As I say the software is used under a strict 'kiosk' type environment with a touchscreen - no keyboard or mouse - so the user has no chance of moving Windows about, accessing the task bar etc (which is hidden in the environment). This may be different for you and the code logic may need changes to cater for user interaction.

Key points for you would be:

1. In the main application WndProc I use WM_DISPLAYCHANGE to detect if a second monitor is unplugged or plugged in while running.
2. In the main application code - the Get2ndMonitorHandle() and EnumDisplayMonitorsProc([...]) would be of key interest.
3. In the AVNDuplicateWindow code - the constructor, OnDisplayChange() and CreateThumbnail([..]) methods would also be of key interest.

If you have any questions feel free to ask  8)


in the main application framework.h header I have these declared:
Code: [Select]
std::unique_ptr<AVNDuplicateWindow> m_pDuplicateWindow;
LRESULT OnDisplayChange(WPARAM wParam, LPARAM lParam);
HMONITOR Get2ndMonitorHandle();
static BOOL EnumDisplayMonitorsProc(HMONITOR hMonitor, HDC hDCMonitor, LPRECT lpRectMonitor, LPARAM dwData);

From the main core .cpp file :
Code: [Select]
LRESULT CALLBACK AVNFramework::_WndProc(HWND hWnd,UINT iMsg,WPARAM wParam,LPARAM lParam) {
AVNFramework* pFramework = reinterpret_cast<AVNFramework*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));

[...]
switch(iMsg){
case WM_DISPLAYCHANGE:
return pFramework->OnDisplayChange(wParam, lParam);
};
[...]
}
[...]

LRESULT AVNFramework::OnDisplayChange(WPARAM wParam, LPARAM lParam)
{
if (m_pDuplicateWindow)
{
RECT Rect;
pVideoDisplay->GetPreviewVideoWindow_PreviewPosition(&Rect);

HMONITOR hMonitor2 = Get2ndMonitorHandle();
m_pDuplicateWindow->OnDisplayChange(hMonitor2, m_hWndMain, Rect);
}

return 0;
}

HMONITOR AVNFramework::Get2ndMonitorHandle()
{
RECT DupRect{};
BOOL FoundSecondMonitor = FALSE;

DISPLAY_DEVICE DisplayDevice = { sizeof(DISPLAY_DEVICE) };
for (DWORD iDevNum = 0; EnumDisplayDevices(nullptr, iDevNum, &DisplayDevice, 0) != FALSE; ++iDevNum)
{
if (DisplayDevice.StateFlags != 0)
{
BOOL IsAttachedToDesktop = (DisplayDevice.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) != 0;
BOOL IsPrimary = (DisplayDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) != 0;
if (IsAttachedToDesktop && !IsPrimary)
{
MonitorFindData MonitorData =
{
DisplayDevice.DeviceName,
nullptr
};
EnumDisplayMonitors(nullptr, nullptr, &EnumDisplayMonitorsProc, reinterpret_cast<LPARAM>(&MonitorData));

if (MonitorData.hMonitor != nullptr)
{
MONITORINFO Info;
Info.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(MonitorData.hMonitor, &Info);

return MonitorData.hMonitor;
}
}
}
}

return NULL;
}

BOOL AVNFramework::EnumDisplayMonitorsProc(HMONITOR hMonitor, HDC hDCMonitor, LPRECT lpRectMonitor, LPARAM dwData)
{
MonitorFindData* MonitorData = reinterpret_cast<MonitorFindData*>(dwData);

MONITORINFOEX Info{};
Info.cbSize = sizeof(MONITORINFOEX);

GetMonitorInfo(hMonitor, &Info);
if (std::wstring(Info.szDevice) == MonitorData->DeviceName)
{
MonitorData->hMonitor = hMonitor;
return FALSE; // return FALSE to stop the enumeration
}

return TRUE; // return TRUE to keep enumerating
}


Somewhere in the main application during initialization startup :
Code: [Select]
RECT Rect;
pVideoDisplay->GetPreviewVideoWindow_PreviewPosition(&Rect);

m_pDuplicateWindow = std::make_unique<AVNDuplicateWindow>(Get2ndMonitorHandle(), m_hWndMain, Rect);

The AVNDuplicateWindow is the instance of the window that will be displayed on the second monitor (MaxGui wraps all of this). It will display full screen on whatever HMONITOR2 instance is handed to it. The RECT in the constructor is the rectangle area of the hWndToDuplicate (source window) that will be duplicated onto the 2nd monitor - ie you can duplicate a portion of the hWnd or of cource the whole thing :

The .h for AVNDuplicateWindow
Code: [Select]
#pragma once

// Used to show a copy of the confidence window on a different monitor

#include <Windows.h>
#include <dwmapi.h>

class AVNDuplicateWindow
{
public:
AVNDuplicateWindow(
HMONITOR Monitor2,
HWND hWndToDuplicate,
RECT& ConfidenceRect);

VOID OnDisplayChange(
HMONITOR Monitor2,
HWND hWndToDuplicate,
RECT& ConfidenceRect);

private:
VOID CreateThumbnail(
HMONITOR Monitor2,
HWND hWndToDuplicate,
RECT& ConfidenceRect);

VOID SetThumbnailProperties(
HMONITOR Monitor2,
RECT& ConfidenceRect);

static LRESULT m_WndProc(
HWND hWnd,
UINT uiMsg,
WPARAM wParam,
LPARAM lParam);

LPCWSTR m_pClassDuplicateConfidence = L"AVNDuplicateConfidence";

HWND m_hWnd = NULL;
HTHUMBNAIL m_hThumbNail = NULL;
};

The cpp file for AVNDuplicateWindow :
Code: [Select]
AVNDuplicateWindow::AVNDuplicateWindow(
HMONITOR Monitor2,
HWND hWndToDuplicate,
RECT& ConfidenceRect)
{
HINSTANCE hInstance = GetModuleHandle(nullptr);

WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.hInstance = GetModuleHandle(nullptr);
wc.hIcon = (HICON)LoadImage(hInstance, MAKEINTRESOURCE(IDI_ICON5), IMAGE_ICON, 64, 64, 0);
wc.hIconSm = (HICON)LoadImage(hInstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, 16, 16, 0);
wc.lpfnWndProc = &AVNDuplicateWindow::m_WndProc;
wc.lpszClassName = m_pClassDuplicateConfidence;
wc.hbrBackground = 0;
wc.cbWndExtra = sizeof(AVNDuplicateWindow*);
wc.style = CS_VREDRAW | CS_HREDRAW;

#ifdef _DEBUG
wc.hCursor = LoadCursor(wc.hInstance, IDC_ARROW);
#endif

ATOM Atom = RegisterClassEx(&wc);
if (!Atom)
return;

RECT Rect{};
DWORD dwStyle = WS_POPUP;
if (Monitor2)
{
MONITORINFO Info = { sizeof(MONITORINFO) };
GetMonitorInfo(Monitor2, &Info);
Rect = Info.rcWork;

dwStyle |= WS_VISIBLE;
}

DWORD Width = Rect.right - Rect.left;
DWORD Height = Rect.bottom - Rect.top;

m_hWnd = CreateWindow(m_pClassDuplicateConfidence, L"", dwStyle, Rect.left, Rect.top, Width, Height, nullptr, nullptr, hInstance, nullptr);
SetWindowLongPtr(m_hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));

if (Monitor2)
CreateThumbnail(Monitor2, hWndToDuplicate, ConfidenceRect);
}

VOID AVNDuplicateWindow::OnDisplayChange(
HMONITOR Monitor2,
HWND hWndToDuplicate,
RECT& ConfidenceRect)
{
if (!Monitor2)
{
DwmUnregisterThumbnail(&m_hThumbNail);
ShowWindow(m_hWnd, SW_HIDE);

m_hThumbNail = NULL;
}
else
CreateThumbnail(Monitor2, hWndToDuplicate, ConfidenceRect);
}

VOID AVNDuplicateWindow::CreateThumbnail(HMONITOR Monitor2, HWND hWndToDuplicate, RECT & ConfidenceRect)
{
DwmRegisterThumbnail(m_hWnd, hWndToDuplicate, &m_hThumbNail);
SetThumbnailProperties(Monitor2, ConfidenceRect);
}

VOID AVNDuplicateWindow::SetThumbnailProperties(
HMONITOR Monitor2,
RECT& ConfidenceRect)
{
MONITORINFO Info{ sizeof(MONITORINFO) };
GetMonitorInfo(Monitor2, &Info);
SetWindowPos(m_hWnd, NULL,
Info.rcWork.left,
Info.rcWork.top,
Info.rcWork.right - Info.rcWork.left,
Info.rcWork.bottom - Info.rcWork.top,
SWP_SHOWWINDOW);

RECT ClientRect;
GetClientRect(m_hWnd, &ClientRect);

DWM_THUMBNAIL_PROPERTIES Props;
Props.dwFlags = DWM_TNP_VISIBLE | DWM_TNP_OPACITY | DWM_TNP_RECTSOURCE | DWM_TNP_RECTDESTINATION;
Props.rcDestination = ClientRect;
Props.rcSource = ConfidenceRect;
Props.opacity = 255;
Props.fVisible = TRUE;

DwmUpdateThumbnailProperties(m_hThumbNail, &Props);
}

LRESULT AVNDuplicateWindow::OnSetCursor(WPARAM wParam, LPARAM lParam)
{
if (LOWORD(lParam) == HTCLIENT)
{
SetCursor(LoadCursor(nullptr, IDC_ARROW));
return TRUE;
}

return DefWindowProc(m_hWnd, WM_SETCURSOR, wParam, lParam);
}

LRESULT AVNDuplicateWindow::m_WndProc(HWND hWnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
AVNDuplicateWindow* pDuplicateWindow = reinterpret_cast<AVNDuplicateWindow*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));

switch (uiMsg)
{
case WM_SETCURSOR:
if (pDuplicateWindow)
return pDuplicateWindow->OnSetCursor(wParam, lParam);

break;
}

return DefWindowProc(hWnd, uiMsg, wParam, lParam);
}
« Last Edit: June 06, 2021, 04:56:11 by col »

Offline Derron

  • Hero Member
  • *****
  • Posts: 3651
Re: How to output different MaxGUI windows to different devices?
« Reply #5 on: June 06, 2021, 07:57:03 »
side note: my chrome browser does not show any screenshots as the links point to HTTP instead of HTTPs image links .. people should not mix http and https ...


bye
Ron

Offline chalky

  • Jr. Member
  • **
  • Posts: 87
Re: How to output different MaxGUI windows to different devices?
« Reply #6 on: June 06, 2021, 12:16:53 »
@Col
Thank you so much for taking the time to post all that - really useful and very much appreciated.

 

SimplePortal 2.3.6 © 2008-2014, SimplePortal