How to output different MaxGUI windows to different devices?

Started by chalky, June 04, 2021, 18:32:42

Previous topic - Next topic

chalky

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?

Henri

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) Select

' redrawgadget.bmx

' version 3 - fixed to be compatible with virtual resolutions

Import MaxGui.Drivers

AutoMidHandle True

Local spinner:TSpinningApplet = New TSpinningApplet.Create()

Repeat
WaitSystem()
Forever

Type TApplet

Method OnEvent(Event:TEvent) Abstract

Method Run()
AddHook EmitEventHook,eventhook,Self
End Method

Function eventhook:Object(id,data:Object,context:Object)
Local event:TEvent = TEvent(data)
Local app:TApplet = TApplet(context)
app.OnEvent( event )
Return data
End Function

End Type

Type TSpinningApplet Extends TApplet

Global image:TImage

Field timer:TTimer, state:Int
Field window:TGadget, canvas:TGadget
Field control:TGadget, button:Tgadget

Method Draw()

SetGraphics CanvasGraphics(canvas)
SetVirtualResolution ClientWidth(canvas),ClientHeight(canvas)
SetViewport 0,0,ClientWidth(canvas), ClientHeight(canvas)

SetBlend( ALPHABLEND )
SetRotation( MilliSecs()*.1 )
SetClsColor( 255, 0, 0 )

Cls()
DrawImage( image, GraphicsWidth()/2, GraphicsHeight()/2 )

Flip()

End Method

Method OnEvent(event:TEvent)
If Not event Then Return
Select event.id
Case EVENT_WINDOWCLOSE, EVENT_APPTERMINATE
End
Case EVENT_TIMERTICK
RedrawGadget( canvas )
Case EVENT_GADGETPAINT
If (event.source = canvas) Then Draw()
Case EVENT_GADGETACTION

If Not state Then
timer.Stop()
state = True
Else
timer = CreateTimer( 100 )
state = False
EndIf

End Select
End Method

Method Create:TSpinningApplet()

If Not image Then image = LoadImage( "fltkwindow.png" )
If Not image Then Notify("Could not load image"); Return

window = CreateWindow( "Clock", 20, 20, 512, 512, Null, WINDOW_CENTER )
control = CreateWindow( "Control", 20, 20, 300, 200 )
button = CreateButton("Start/Stop", 20, 20, 60, 30, control)
Local w = ClientWidth(window)
Local h = ClientHeight(window)

canvas = CreateCanvas( 0, 0, w, h, window )
SetGadgetLayout( canvas, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED )

timer = CreateTimer( 100 )
Run()

Return Self

End Method

End Type


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

col

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.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

chalky

@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.

col

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:

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 :

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 :

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

#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 :

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);
}
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Derron

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

chalky

@Col
Thank you so much for taking the time to post all that - really useful and very much appreciated.