MDI behavior in WPF? - wpf

I'm writing an industrial process control application to run on a PC, using .Net. The program monitors the progress of various parts being assembled by teams on the factory floor. There can be an arbitrary number of parts - 1,2,3,4,5, etc, and in the old VB6 version of the app each part gets its own window and the operators like to arrange them on the screen.
What I'm describing is a classic MDI interface but WPF doesn't support MDI. Other threads on SO have suggested the wpfmdi project on Codeplex, but that's listed as "abandoned" since last February ( http://wpfmdi.codeplex.com ) and avalondocks but those are docking tiles that don't look like they can be arbitrarily dragged and moved.
I don't know what to do. I didn't want to use WinForms because WPF/XAML provides cooler visuals and easier styling and because Microsoft seems to have abandoned WinForms. The current VB6 version of this product is 12 years old and I'd like to plan on a similar lifespan for the new one.
Thanks in advance!

I think you should consider using a payed-for third party component for MDI support. Nearly all of the standard vendors, DevExpress, Component One, Infragisitcs, Telerik provide an MDI solution.
Personally, I think MDI is still an entirely valid application UI structure!

I found the answer on another discussion forum (I can't remember which one or I'd give them credit). It turned out to be easier than I thought. If you hook the WM_MOVING message (I do it, below when the window is loaded) you can intercept moves before the window is moved and constrain the location of the window.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WindowInteropHelper helper = new WindowInteropHelper(this);
HwndSource.FromHwnd(helper.Handle).AddHook(HwndMessageHook);
InitialWindowLocation = new Point(this.Left, this.Top);
}
// Grab the Win32 WM_MOVING message so we can intercept a move BEFORE
// it happens and constrain the child window's location.
private IntPtr HwndMessageHook(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool bHandled)
{
switch (msg)
{
// might also want to handle case WM_SIZING:
case WM_MOVING:
{
WIN32Rectangle rectangle = (WIN32Rectangle)Marshal.PtrToStructure(lParam, typeof(WIN32Rectangle));
if (rectangle.Top < 50)
{
rectangle.Top = 50;
rectangle.Bottom = 50 + (int)this.Height;
bHandled = true;
}
if (rectangle.Left < 10)
{
rectangle.Left = 10;
rectangle.Right = 10 + (int)this.Width;
bHandled = true;
}
if (rectangle.Bottom >800)
{
rectangle.Bottom = 800;
rectangle.Top = 800 - (int)this.Height;
bHandled = true;
}
// do anything to handle Right case?
if (bHandled)
{
Marshal.StructureToPtr(rectangle, lParam, true);
}
}
break;
}
return IntPtr.Zero;
}
The XAML header looks like this:
<Window x:Class="Mockup_9.Entity11"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Mockup_9"
ShowInTaskbar="False"
Background="LightGoldenrodYellow"
Loaded="Window_Loaded"
Title="Mockup_part -" Height="540" Width="380" ResizeMode="NoResize"
Icon="/Mockup_9;component/Images/refresh-icon1.jpg">
. . . etc.

Related

Minimize window using Windows 11 animation effect

There are two (it would seem) idiomatic ways to minimize a WPF window in code:
window.WindowState = window.WindowState.Minimized
SystemCommands.MinimizeWindow(window)
The desired behavior is that the windows 11 window minimize animation is seen. The actual behavior is that the window instantly disappears from the screen.
Please note: This is for a window with a custom chrome - therefore triggering the behavior from code seems necessary:
<Window
WindowStyle="None"
AllowTransparency="True
Background="Transparent">
<!-- custom windows chrome here -->
</Window>
Is this a defect in the WPF desktop pack for .NET 5 (and 6)?
How does one not get the desired behavior without reverting back to default windows with standard chrome? It is not an option to use a WinUI 3.0 window since XAML islands have not been released yet and gradual modernization is needed.
There are some proposed solutions on the web, like:
https://coderedirect.com/questions/352460/custom-window-style-with-minimize-animation
And the other (old) stackoverflow question:
Custom window style with minimize animation
However, I could not get it to work with a window with a transparent background.
===== update after issue was closed =====
Reviewer: please read this carefully before voting to close the issue again and point to exact workable solution.
Issues with existing answers:
First suggestion is to change WindowStyle, then WindowState; this poses a problem since it requires AllowTransparency=false which defeats the purpose of the exercise here - which is getting a custom chrome.
Other suggestion is to use Win32 API's. Again I tried to do that but it did not work in my setting. I haven't logged the exact error messages I got though. However, looking at the code again there is an obvious issue with it: it has the same window requirement as the first suggestion: the window must not be transparent - observe code from the old answer:
private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == ApiCodes.WM_SYSCOMMAND)
{
if (wParam.ToInt32() == ApiCodes.SC_MINIMIZE)
{
WindowStyle = WindowStyle.SingleBorderWindow;
WindowState = WindowState.Minimized;
handled = true;
}
else if (wParam.ToInt32() == ApiCodes.SC_RESTORE)
{
WindowState = WindowState.Normal;
WindowStyle = WindowStyle.None;
handled = true;
}
}
return IntPtr.Zero;
}
It seems to be on the roadmap in Themes of .NET for .NET 7:
Epic - Support Windows 11 look and feel for WPF controls
Also this amazing piece of open-source just landed:
WPF UI - Windows 11 styles and controls for WPF

How to find a WinForms control's owner form if it is nested into ElementHost?

One of our customers hosts our WinForms .NET grid control iGrid.NET (http://www.10tec.com/) inside a WPF ElementHost container together with other WPF controls. It may look strange as it's a WinForms control inside a WPF host inside a WinForms form, but they have no choice because of the other WPF stuff they use (it's the AvalonDock http://avalondock.codeplex.com/ docking container).
The problem is that our .NET datagrid control's infrastructure requires to know the parent WinForms form, but the following construction we use for that always return null in this situation:
Form myTopLevelOwnerForm = fCurrentGrid.TopLevelControl as Form;
I.e. the standard Control.TopLevelControl property intended for this purpose returns null - though most likely it should be so in the case of WPF host.
The question is: are there other ways to know the parent form from the current control's code? Say, using WinAPI handles or better other native .NET memebrs?
The following code works. At least, in our project :)
// API declaration
[System.Runtime.InteropServices.DllImport("user32.dll", ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
public static extern IntPtr GetParent(IntPtr hWnd);
// Main code snippet
Control myTopLevelControl = fOwner.TopLevelControl;
if (myTopLevelControl == null)
{
IntPtr handle = fOwner.Handle;
while (true)
{
IntPtr parentHandle = GetParent(handle);
if (parentHandle == IntPtr.Zero)
{
myTopLevelControl = Control.FromHandle(handle) as Form;
break;
}
handle = parentHandle;
}
}

Dockable Windows. Floating Window and MainWindow Menu Integration

In Visual Studio 2010, Dockable Windows seem to work like expected in every situation.
If a "Floating" document is active and some menu is selected (e.g Edit -> Paste), then the "Floating" document still has Focus and the command will be executed against that "Floating" window. Also, notice how this is clearly visible in the UI. MainWindow.xaml is still active and the Main window in Visual Studio is inactive even though the Team-menu is selected.
I've been trying to get the same behavior using alot of different 3rd-party docking components but they all have the same problem: once I select the menu, the MainWindow is focused and my floating window does not have focus anymore. Does anyone know of a way to get the same behavior here as in Visual Studio?
At the moment I'm using Infragistics xamDockManager and the problem can be reproduced with the following sample code.
Right click "Header 1" and select "Float"
Click the "File" menu
Notice how MainWindow receives focus.
xmlns:igDock="http://infragistics.com/DockManager"
<DockPanel LastChildFill="True">
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New"/>
</MenuItem>
</Menu>
<Grid>
<igDock:XamDockManager x:Name="dockManager" Theme="Aero">
<igDock:DocumentContentHost>
<igDock:SplitPane>
<igDock:TabGroupPane>
<igDock:ContentPane Header="Header 1">
<TextBox Text="Some Text"/>
</igDock:ContentPane>
<igDock:ContentPane Header="Header 2">
<TextBox Text="Some Other Text"/>
</igDock:ContentPane>
</igDock:TabGroupPane>
</igDock:SplitPane>
</igDock:DocumentContentHost>
</igDock:XamDockManager>
</Grid>
</DockPanel>
The visual studio team has some good information on lessons they learned when making VS in WPF. One of the issues they ran into was related to Focus management. As a result, WPF 4 has some new features to help out.
Here's the info on the issue that sounds like your situation:
http://blogs.msdn.com/b/visualstudio/archive/2010/03/09/wpf-in-visual-studio-2010-part-3-focus-and-activation.aspx
Their discussion of the new "HwndSource.DefaultAcquireHwndFocusInMenuMode" property sounds very similar to what you're running into.
EDIT
After further investigation, it looks like Visual Studio might be hooking the windows message loop and returning specific values to make the floating windows work.
I'm not a win32 programmer, but it seems that when a user clicks a menu in an inactive window, windows sends the WM_MOUSEACTIVATE message to it before processing the mouse down event. This lets the main window determine whether it should be activated.
In my unmodified WPF test app, the inactive window returns MA_ACTIVATE. However, VS returns MA_NOACTIVATE. The docs indicate that this tells windows NOT to activate the main window prior to handling further input. I'm guessing that visual studio hooks the windows message loop and returns MA_NOACTIVATE when the user clicks on the menus / toolbars.
I was able to make this work in a simple, two window WPF app by adding this code to the top level window.
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
var hook = new HwndSourceHook(this.FilterMessage);
var source2 = HwndSource.FromVisual(this) as HwndSource;
source2.AddHook(hook);
}
private IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_MOUSEACTIVATE = 0x0021;
const int MA_NOACTIVATE = 3;
switch (msg)
{
case WM_MOUSEACTIVATE:
handled = true;
return new IntPtr(MA_NOACTIVATE);
}
return IntPtr.Zero;
}
In your case, you'd probably need to add more logic that would check what the user clicked on and decide based on that whether to intercept the message and return MA_NOACTIVATE.
EDIT 2
I've attached a sample WPF application that shows how to do this with a simple WPF application. This should work pretty much the same with floating windows from a docking toolkit, but I haven't tested that specific scenario.
The sample is available at: http://blog.alner.net/downloads/floatingWindowTest.zip
The sample has code comments to explain how it works. To see it in action, run the sample, click the "open another window" button. This should put focus in the textbox of the new window. Now, click the edit menu of the main window and use the commands like "select all". These should operate on the other window without bringing the "main window" to the foreground.
You can also click on the "exit" menu item to see that it can still route commands to the main window if needed.
Key Points (Activation / Focus):
Use the HwndSource.DefaultAcquireHwndFocusInMenuMode to get the menus to work stop grabbing focus.
Hook the message loop and return "MA_NOACTIVATE" when the user clicks the menu.
Add an event handler to the menu's PreviewGotKeyboardFocus and set e.Handled to true so that the menu wont' attempt to grab focus.
Key Points (Commands):
Hook the main window's "CommandManager.PreviewCanExecute" and "CommandManager.PreviewExecuted" events.
In these events, detect whether the app has an "other window" that's supposed to be the target of events.
Manually invoke the original command against the "other window".
Hope it works for you. If not, let me know.
I used the great answer from NathanAW and created a ResourceDictionary containing a Style for Window (which should be used by the MainWindow), contained the key pieces to solve this problem.
Update: Added support for ToolBar as well as Menu
It includes hit testing specifically for the MainMenu or ToolBar to decide if focusing should be allowed.
The reason I've used a ResourceDictionary for this is for reusability since we will be using this in many projects. Also, the code behind for the MainWindow can stay clean.
MainWindow can use this style with
<Window...>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="NoFocusMenuWindowDictionary.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.Style>
<StaticResource ResourceKey="NoFocusMenuWindow"/>
</Window.Style>
<!--...-->
</Window>
NoFocusMenuWindowDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MainWindowVS2010Mode.NoFocusMenuWindowDictionary">
<Style x:Key="NoFocusMenuWindow" TargetType="Window">
<EventSetter Event="Loaded" Handler="MainWindow_Loaded"/>
</Style>
<Style TargetType="Menu">
<EventSetter Event="PreviewGotKeyboardFocus"
Handler="Menu_PreviewGotKeyboardFocus"/>
</Style>
<Style TargetType="ToolBar">
<EventSetter Event="PreviewGotKeyboardFocus"
Handler="ToolBar_PreviewGotKeyboardFocus"/>
</Style>
</ResourceDictionary>
NoFocusMenuWindowDictionary.xaml.cs
namespace MainWindowVS2010Mode
{
public partial class NoFocusMenuWindowDictionary
{
#region Declaration
private static Window _mainWindow;
private static bool _mainMenuOrToolBarClicked;
#endregion // Declaration
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_mainWindow = sender as Window;
HwndSource.DefaultAcquireHwndFocusInMenuMode = true;
Keyboard.DefaultRestoreFocusMode = RestoreFocusMode.None;
HwndSource hwndSource = HwndSource.FromVisual(_mainWindow) as HwndSource;
hwndSource.AddHook(FilterMessage);
}
private static IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_MOUSEACTIVATE = 0x0021;
const int MA_NOACTIVATE = 3;
switch (msg)
{
case WM_MOUSEACTIVATE:
if (ClickedMainMenuOrToolBarItem())
{
handled = true;
return new IntPtr(MA_NOACTIVATE);
}
break;
}
return IntPtr.Zero;
}
#region Hit Testing
private static bool ClickedMainMenuOrToolBarItem()
{
_mainMenuOrToolBarClicked = false;
Point clickedPoint = Mouse.GetPosition(_mainWindow);
VisualTreeHelper.HitTest(_mainWindow,
null,
new HitTestResultCallback(HitTestCallback),
new PointHitTestParameters(clickedPoint));
return _mainMenuOrToolBarClicked;
}
private static HitTestResultBehavior HitTestCallback(HitTestResult result)
{
DependencyObject visualHit = result.VisualHit;
Menu parentMenu = GetVisualParent<Menu>(visualHit);
if (parentMenu != null && parentMenu.IsMainMenu == true)
{
_mainMenuOrToolBarClicked = true;
return HitTestResultBehavior.Stop;
}
ToolBar parentToolBar = GetVisualParent<ToolBar>(visualHit);
if (parentToolBar != null)
{
_mainMenuOrToolBarClicked = true;
return HitTestResultBehavior.Stop;
}
return HitTestResultBehavior.Continue;
}
public static T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
#endregion // Hit Testing
#region Menu
private void Menu_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Menu menu = sender as Menu;
if (menu.IsMainMenu == true)
{
e.Handled = true;
}
}
#endregion // Menu
#region ToolBar
private void ToolBar_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
e.Handled = true;
}
#endregion // ToolBar
}
}
Just out of curiosity, have you tried binding the MenuItem.CommandTarget to the XamDockManager.ActivePane?
Looking at the XamDockManager documentation, I also see a CurrentFlyoutPane property which returns "the Infragistics.Windows.DockManager.ContentPane currently within the UnpinnedTabFlyout or null if the flyout is not shown." I'm not sure which property would be appropriate in your scenario, but it's worth a try.
I know this is an old post, but Prism could make your life so much easier. Using the RegionAdapter created here:
http://brianlagunas.com/2012/09/12/xamdockmanagera-prism-regionadapter/
You can easily track which window is active, floating or not, by using the IActiveAware interface. Prisms commands also take this into consideration and can excute commands only on the active view. The blog post has a sample app you can play around with.
Im not sure about how to make this work, but I do know the Infragistics have a great support forum so it may be worth asking there question there too.
http://forums.infragistics.com/

Get drag & drop to work in WPF which already works in Windows Forms

I am trying to use this function from a COM API which enables the window to receive drops (as in drag & dop) from another application.
It is pretty straightforward in Windows Forms and works:
public void EnableDropSupport(System.Windows.Forms.Form form)
{
IntPtr hwnd = form.Handle;
_comAPI.RegisterDropWindow((int)hwnd);
}
But I have a WPF window where it does not work and I don't understand why. I have tried the following:
public void EnableDropSupport(System.Windows.Window window)
{
window.AllowDrop = true;
WindowInteropHelper windowInteropHelper = new WindowInteropHelper(window);
IntPtr hwnd = windowInteropHelper.Handle;
_comAPI.RegisterDropWindow((int)hwnd);
}
The last two lines are basically identical but it will just not work in WPF. While
window.AllowDrop = true;
will make it appear as if it will accept the drop, the drop event of that COM API is not raised.
Am I missing something or can someone help?
This is Pavel Minaev's answer (which he posted as a comment to the question) which was correct:
You're not missing anything on WPF
side of things. Most likely the
problem is with RegisterDropWindow.

WPF mutli-monitor problem - WindowState

I've been trying to get my WPF application to span multiple monitors for some time now and nearly have it working.
The problem seems to arise when I set the following line:
win1.WindowState = WindowState.Maximized
This causes the application to span only the primary screen.
My code is as follows:
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
Window1 win1 = new Window1();
win1.WindowStartupLocation = WindowStartupLocation.Manual;
win1.Width = 2560;
win1.Height = 1024;
win1.Left = 0;
win1.Top = 0;
win1.Topmost = true;
win1.Background = new SolidColorBrush(Colors.Black);
win1.WindowStyle = WindowStyle.None;
win1.Show();
win1.Focus();
}
}
And inside of Window 1:
public partial class Window1 : Window
{
public Window1()
{
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Maximized;
}
}
This example works, but the window is not maximized, and the application borders are still visible.
Including the maximized deceleration in Application_Startup makes the monitor maximize to the primary monitor.
Why is this?
First note that the concept of "Maximized" is tied to a single monitor, so you cannot truly have a maximized window on multiple monitors. Of course in WPF you can create your own window frame and draw anything you like in it, so if you want you can certainly make the user think the window is maximized and spanning multiple screens.
Also note that it is possible to span two monitors with a single rectangular window in only two cases:
The two monitors have the same height and are configured to be side by side, or
The two monitors have the same width and are configured to be above and below.
Otherwise you will need to use two separate windows to cover the entire surfaces of both monitors, or use a large window that includes areas that aren't covered by any monitor.
Ok, here's how to get the information you'll need to position your window(s):
WPF itself does not provide a way to ask about your monitor count, resolutions, or relative positions. Fortunately we can call Win32 directly using [DllImport]. To get monitor resolutions and layouts, just:
Declare the MONITORINFO struct as a struct in C#
Declare DllImports for EnumDisplayMonitors and GetMonitorInfo, both found in User32.dll
Write a method that calls EnumDisplayMonitors and passes a delegate that gets the monitor info and returns it in a list.
Here is the basic idea:
List<MONITORINFO> GetAllMonitorInfo()
{
var result = List<MONITORINFO>();
EnumDisplayMonitors(null, null,
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
var info = new MONITORINFO { cbSize = Marshall.Sizeof(typeof(MONITORINFO)) };
GetMonitorInfo(hMonitor, ref info);
result.Add(info);
}, null);
return result;
}
Once you have the monitor coordinates, use an algorithm of your choice to select how many window(s) you want to create and what coordinates you want for each one. Then create the windows using explicit size(s) and location(s).
Note that you'll probably want to use rcWork as opposed to rcMonitor so you don't overwrite the start menu, etc.
Also note that in many cases some of the coordinates returned will be negative, for example if the secondary monitor is to the left of the primary monitor. This is not a problem: Just use the coordinates as given and your windows will appear in the correct places.
If you can always assume that your secondary monitor is at the same resolution as the primary, you could implement something like this:
// Use this is you are concerned about the taskbar height
Rect workArea = SystemParameters.WorkArea;
this.Width = SystemParameters.PrimaryScreenWidth * 2;
this.Height = workArea.Bottom;
this.Left = 0;
this.Top = 0;
Or:
// Use this is you don't care about the taskbar height
this.Width = SystemParameters.PrimaryScreenWidth * 2;
this.Height = SystemParameters.PrimaryScreenHeight;
this.Left = 0;
this.Top = 0;

Resources