How Can I Only Allow Uniform Resizing in a WPF Window? - c#

I don't want my window to be resized either "only horizontally" or "only vertically." Is there a property I can set on my window that can enforce this, or is there a nifty code-behind trick I can use?

You can always handle the WM_WINDOWPOSCHANGING message, this let's you control the window size and position during the resizing process (as opposed to fixing things after the user finished resizing).
Here is how you do it in WPF, I combined this code from several sources, so there could be some syntax errors in it.
internal enum WM
{
WINDOWPOSCHANGING = 0x0046,
}
[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public int flags;
}
private void Window_SourceInitialized(object sender, EventArgs ea)
{
HwndSource hwndSource = (HwndSource)HwndSource.FromVisual((Window)sender);
hwndSource.AddHook(DragHook);
}
private static IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
{
switch ((WM)msg)
{
case WM.WINDOWPOSCHANGING:
{
WINDOWPOS pos = (WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));
if ((pos.flags & (int)SWP.NOMOVE) != 0)
{
return IntPtr.Zero;
}
Window wnd = (Window)HwndSource.FromHwnd(hwnd).RootVisual;
if (wnd == null)
{
return IntPtr.Zero;
}
bool changedPos = false;
// ***********************
// Here you check the values inside the pos structure
// if you want to override tehm just change the pos
// structure and set changedPos to true
// ***********************
if (!changedPos)
{
return IntPtr.Zero;
}
Marshal.StructureToPtr(pos, lParam, true);
handeled = true;
}
break;
}
return IntPtr.Zero;
}

You can reserve aspect ratio of contents using WPF's ViewBox with control with fixed width and height inside.
Let's give this a try. You can change "Stretch" attribute of ViewBox to experience different results.
Here is my screeen shot:
<Window x:Class="TestWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Viewbox Stretch="Uniform">
<StackPanel Background="Azure" Height="400" Width="300" Name="stackPanel1" VerticalAlignment="Top">
<Button Name="testBtn" Width="200" Height="50">
<TextBlock>Test</TextBlock>
</Button>
</StackPanel>
</Viewbox>
</Window>

This is what my solution was.
You will need to add this to your control/window tag:
Loaded="Window_Loaded"
And you will need to place this in your code behind:
private double aspectRatio = 0.0;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
aspectRatio = this.ActualWidth / this.ActualHeight;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
if (sizeInfo.WidthChanged)
{
this.Width = sizeInfo.NewSize.Height * aspectRatio;
}
else
{
this.Height = sizeInfo.NewSize.Width * aspectRatio;
}
}
I tried the Viewbox trick and I did not like it. I wanted to lock the window border to a specific size. This was tested on a window control but I assume it would work on a border as well.

You could try replicating an effect that I often see on Flash Video websites. They allow you to expand the browser window any way you like, but only stretch the presentation area so that it fits the smallest of the height or width.
For example, if you stretch the window vertically, your application would not resize. It would simple add black bars to the top and bottom of the display area and remain vertically centered.
This may or may not be possible with WPF; I don't know.

This may be bit late but you can simply put it in your code behind....
Private Sub UserControl1_SizeChanged(ByVal sender As Object, ByVal e As System.Windows.SizeChangedEventArgs) Handles Me.SizeChanged
If e.HeightChanged Then
Me.Width = Me.Height
Else
Me.Height = Me.Width
End If
End Sub

I had expected that you could two-way bind the width to the height using a value converter to maintain aspect ratio. Passing the aspect ratio as the converter parameter would make it more general purpose.
So, I tried this - the binding with no converter first:
<Window
...
Title="Window1" Name="Win" Height="500"
Width="{Binding RelativeSource={RelativeSource self},
Path=Height, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<StackPanel>
<TextBlock>Width:</TextBlock>
<TextBlock Text="{Binding ElementName=Win, Path=Width}" />
<TextBlock>Height:</TextBlock>
<TextBlock Text="{Binding ElementName=Win, Path=Height}" />
</StackPanel>
</Window>
Strangely, the binding is behaving as if it is one-way and the reported width of the window (as shown in the TextBlock) is not consistent with it's size on screen!
The idea might be worth pursuing, but this strange behavior would need to be sorted out first.
Hope that helps!

In the code sample:
if (sizeInfo.WidthChanged)
{
this.Width = sizeInfo.NewSize.Height * aspectRatio;
}
else
{
this.Height = sizeInfo.NewSize.Width * aspectRatio;
}
I believe the second computation should be:
this.Height = sizeInfo.NewSize.Width * (1/aspectRatio);
I made a variation of this work in a "SizeChanged" event handler. Since I wanted the width to be the controlling dimension, I simply forced the height to match to it with a computation of the form:
if (aspectRatio > 0)
// enforce aspect ratio by restricting height to stay in sync with width.
this.Height = this.ActualWidth * (1 / aspectRatio);
You may note the check for an aspectRatio > 0, ... I did this because I found that it was tending to call my handlers that did the resizing before the "Load" method had even assigned the aspectRatio.

Maybe too late, but i found a solution from Mike O'Brien blog, and it work really good.
http://www.mikeobrien.net/blog/maintaining-aspect-ratio-when-resizing/
Below is code from his blog:
<Window ... SourceInitialized="Window_SourceInitialized" ... >
...
Window>
public partial class Main : Window
{
private void Window_SourceInitialized(object sender, EventArgs ea)
{
WindowAspectRatio.Register((Window)sender);
}
...
}
internal class WindowAspectRatio
{
private double _ratio;
private WindowAspectRatio(Window window)
{
_ratio = window.Width / window.Height;
((HwndSource)HwndSource.FromVisual(window)).AddHook(DragHook);
}
public static void Register(Window window)
{
new WindowAspectRatio(window);
}
internal enum WM
{
WINDOWPOSCHANGING = 0x0046,
}
[Flags()]
public enum SWP
{
NoMove = 0x2,
}
[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public int flags;
}
private IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
{
if ((WM)msg == WM.WINDOWPOSCHANGING)
{
WINDOWPOS position = (WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));
if ((position.flags & (int)SWP.NoMove) != 0 ||
HwndSource.FromHwnd(hwnd).RootVisual == null) return IntPtr.Zero;
position.cx = (int)(position.cy * _ratio);
Marshal.StructureToPtr(position, lParam, true);
handeled = true;
}
return IntPtr.Zero;
}
}

I got a way that doesn't depend on Windows platform-specific API also with acceptable user experience (not shake while dragging the window). It uses a timer to adjust the window size after 0.1 seconds so user won't see it shakes.
public partial class MainWindow : Window
{
private DispatcherTimer resizeTimer;
private double _aspectRatio;
private SizeChangedInfo? _sizeInfo;
public MainWindow()
{
InitializeComponent();
_aspectRatio = Width / Height;
resizeTimer = new DispatcherTimer();
resizeTimer.Interval = new TimeSpan(100*10000); // 0.1 seconds
resizeTimer.Tick += ResizeTimer_Tick;
}
private void ResizeTimer_Tick(object? sender, EventArgs e)
{
resizeTimer.Stop();
if (_sizeInfo == null) return;
var percentWidthChange = Math.Abs(_sizeInfo.NewSize.Width - _sizeInfo.PreviousSize.Width) / _sizeInfo.PreviousSize.Width;
var percentHeightChange = Math.Abs(_sizeInfo.NewSize.Height - _sizeInfo.PreviousSize.Height) / _sizeInfo.PreviousSize.Height;
if (percentWidthChange > percentHeightChange)
this.Height = _sizeInfo.NewSize.Width / _aspectRatio;
else
this.Width = _sizeInfo.NewSize.Height * _aspectRatio;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
_sizeInfo = sizeInfo;
resizeTimer.Stop();
resizeTimer.Start();
base.OnRenderSizeChanged(sizeInfo);
}
}

Related

Is there a way to change the color of the NonClientFrameEdges or make them invisible (particularly with WPF/.NET)?

Goal Summary
I'm trying to make a WPF window with a custom window caption (so I can paint the title bar and add controls there). I've been successful in doing this with the WPF WindowChrome class, but it introduces a lot of bugs. I've been able to work around most of them by setting the WindowChrome NoneClientFrameEdges property to any value other than None, but this introduced a new bug in the process. There's an unsightly ~1px thick border where the NonClientFrameEdge property is set. You can see it in the video, but it's very faint. I want to either set the color to transparent or find a way to disable rendering it entirely. The problem is that I can't actually remove the NonClientFrameEdge because it's required to fix the bugs I mentioned earlier (more details below).
Detailed explanation of the problem (as best as I can understand it)
From what I understand, a standard window is separated into at least two parts: the client area and the non-client area. The client area is the part of the window you can easily customize, where you add your controls and other window content. The non-client area is the part of the window that includes the frame, which consists of the resize borders and the caption. Under normal conditions you can’t modify the non-client area.
The WindowChrome class in WPF provides an easy way to customize the non-client area of a WPF window. This enables you to design your own caption and place controls there like you see in modern enterprise apps such as Visual Studio and Microsoft Office. The problem is that using WindowChrome causes many bugs. Here’s a list of the ones I’ve found so far:
When you minimize the window from a maximized state and hover over it in the taskbar, the resulting window preview has ~8 px of empty space on the top and top-left sides. If you click it to bring it to the front, it snaps back to the proper space. Even Visual Studio and Microsoft Office have this bug. (caused by having GlassFrameThickness value of 0 set in WindowChrome class)
The transparent glass rectangle effect you get when hovering over the aero peek button for “peek at desktop” no longer works. You just see empty space, no outline of the app. (caused by having GlassFrameThickness value of 0 set in WindowChrome class)
The window is jittery when resizing from any corner other than the bottom right. I’ve read this is because resizing from these corners forces a position change of the window. (caused by having NonClientFrameEdges value of 0, at least 1 edge needs to be set to avoid this)
The window is distorted and blurry during the focus zoom effect when selecting a minimized window from task view. Even many enterprise apps have this problem. (Adobe Photoshop, Visual Studio, and several game launchers, to name a few). (caused either by GlassFrameThickness value of 0, NonClientFrameEdges value of None, or a combination of the two)
Hacky solution and video before and after
I noticed that issue 1 and 2 can be fixed by setting the GlassFrameThickness to a non-zero value. Issues 3 and 4 can be fixed by setting the NonClientFrameEdges property of the WindowChrome class to any value other than None. This video shows all of the problems listed above, along with how they look before and after changing that property: https://www.youtube.com/watch?v=z7O28aEPygg
Note: The video ends abruptly due to my poor editing skills, but it should show all the necessary issues. You will have to look very closely at the bottom of the app in the second half of the video to see the 1px grey/white NonClientFrameEdge, but it's noticeable. This is what I'm trying to fix.
The problem is that setting NonClientFrameEdges to any value other than None literally adds a 2-3 px edge to your window, and it's visually obvious. Setting GlassFrameThickness to 1 on the same edge as the NonClientFrameEdge reduces the visibility significantly, but it's still a noticeable 1px or so eyesore. For example if NonClientFrameEdge is set to Bottom, GlassFrameThickness should be set to "0, 0, 0, 1".
Minimal Example
You can reproduce the problem by creating a .NET or .NET Core WPF project and adding the following code to the window's XAML view file:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="0 0 0 1" CornerRadius="0" CaptionHeight="38" UseAeroCaptionButtons="False" ResizeBorderThickness="5" NonClientFrameEdges="Bottom" />
</WindowChrome.WindowChrome>
Here's a Git Repo with a minimal project for convenience if you want it to test but don't feel like typing all the code. It includes some extra stuff like the boilerplate for the ViewModel, command firing, buttons for min/max/close, and a button to add a border for testing (makes things like the resize jitter more apparent). It also includes a hook into WndProc with Pinvoke just in case you want to experiment with the WindowsAPI: https://github.com/cjfcode/WindowProject
The key to solving this is to wire up a window hook with a handler for the WM_NCCALCSIZE (0x83) and WM_NCPAINT (0x85) messages.
WM_NCPAINT will allow you to remove the single pixel bottom border by calling DwmExtendFrameIntoClientArea. In the code below I've wrapped that call in a method called RemoveFrame.
WM_NCCALCSIZE will allow you to change the size of the client area of the window restoring the extra space that WindowChrome set by using GlassFrameThickness="0,0,0,1" and NonClientFrameEdges="Bottom".
I've wrapped up this functionality into a XAML behavior.
Here is final code that will solve your issue:
WindowChromeLoadedBehavior
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Shell;
using Microsoft.Xaml.Behaviors;
namespace WpfApp1
{
public class WindowChromeLoadedBehavior : Behavior<FrameworkElement>
{
private Window window;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnLoaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
window = Window.GetWindow(AssociatedObject);
if (window == null) return;
Task.Delay(5).ContinueWith(_ =>
{
Dispatcher.Invoke(() =>
{
var oldWindowChrome = WindowChrome.GetWindowChrome(window);
if (oldWindowChrome == null) return;
var newWindowChrome = new WindowChrome
{
CaptionHeight = oldWindowChrome.CaptionHeight,
CornerRadius = oldWindowChrome.CornerRadius,
GlassFrameThickness = new Thickness(0, 0, 0, 1),
NonClientFrameEdges = NonClientFrameEdges.Bottom,
ResizeBorderThickness = oldWindowChrome.ResizeBorderThickness,
UseAeroCaptionButtons = oldWindowChrome.UseAeroCaptionButtons
};
WindowChrome.SetWindowChrome(window, newWindowChrome);
});
});
var hWnd = new WindowInteropHelper(window).Handle;
HwndSource.FromHwnd(hWnd)?.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case NativeMethods.WM_NCPAINT:
RemoveFrame();
handled = false;
break;
case NativeMethods.WM_NCCALCSIZE:
handled = false;
var rcClientArea = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));
rcClientArea.Bottom += (int)(WindowChromeHelper.WindowResizeBorderThickness.Bottom / 2);
Marshal.StructureToPtr(rcClientArea, lParam, false);
var retVal = IntPtr.Zero;
if (wParam == new IntPtr(1))
{
retVal = new IntPtr((int)NativeMethods.WVR.REDRAW);
}
return retVal;
}
return IntPtr.Zero;
}
private void RemoveFrame()
{
if (Environment.OSVersion.Version.Major >= 6 && NativeMethods.IsDwmAvailable())
{
if (NativeMethods.DwmIsCompositionEnabled() && SystemParameters.DropShadow)
{
NativeMethods.MARGINS margins;
margins.bottomHeight = -1;
margins.leftWidth = 0;
margins.rightWidth = 0;
margins.topHeight = 0;
var helper = new WindowInteropHelper(window);
NativeMethods.DwmExtendFrameIntoClientArea(helper.Handle, ref margins);
}
}
}
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public static RECT Empty;
public int Width => Math.Abs(Right - Left);
public int Height => (Bottom - Top);
public RECT(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public RECT(RECT rcSrc)
{
Left = rcSrc.Left;
Top = rcSrc.Top;
Right = rcSrc.Right;
Bottom = rcSrc.Bottom;
}
public RECT(Rectangle rectangle) : this(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom)
{
}
public bool IsEmpty
{
get
{
if (Left < Right)
{
return (Top >= Bottom);
}
return true;
}
}
public override string ToString()
{
if (this == Empty)
{
return "RECT {Empty}";
}
return string.Concat("RECT { left : ", Left, " / top : ", Top, " / right : ", Right, " / bottom : ", Bottom, " }");
}
public override bool Equals(object obj)
{
return ((obj is RECT) && (this == ((RECT)obj)));
}
public override int GetHashCode()
{
return ((Left.GetHashCode() + Top.GetHashCode()) + Right.GetHashCode()) + Bottom.GetHashCode();
}
public static bool operator ==(RECT rect1, RECT rect2)
{
return ((((rect1.Left == rect2.Left) && (rect1.Top == rect2.Top)) && (rect1.Right == rect2.Right)) && (rect1.Bottom == rect2.Bottom));
}
public static bool operator !=(RECT rect1, RECT rect2)
{
return !(rect1 == rect2);
}
static RECT()
{
Empty = new RECT();
}
}
}
}
WindowChromeHelper
using System;
using System.Runtime.InteropServices;
using System.Windows;
namespace WpfApp1
{
public static class WindowChromeHelper
{
public static Thickness LayoutOffsetThickness => new Thickness(0d, 0d, 0d, SystemParameters.WindowResizeBorderThickness.Bottom);
/// <summary>
/// Gets the properly adjusted window resize border thickness from system parameters.
/// </summary>
public static Thickness WindowResizeBorderThickness
{
get
{
var dpix = GetDpi(GetDeviceCapsIndex.LOGPIXELSX);
var dpiy = GetDpi(GetDeviceCapsIndex.LOGPIXELSY);
var dx = GetSystemMetrics(GetSystemMetricsIndex.CXFRAME);
var dy = GetSystemMetrics(GetSystemMetricsIndex.CYFRAME);
// This adjustment is needed since .NET 4.5
var d = GetSystemMetrics(GetSystemMetricsIndex.SM_CXPADDEDBORDER);
dx += d;
dy += d;
var leftBorder = dx / dpix;
var topBorder = dy / dpiy;
return new Thickness(leftBorder, topBorder, leftBorder, topBorder);
}
}
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
private static float GetDpi(GetDeviceCapsIndex index)
{
var desktopWnd = IntPtr.Zero;
var dc = GetDC(desktopWnd);
float dpi;
try
{
dpi = GetDeviceCaps(dc, (int)index);
}
finally
{
ReleaseDC(desktopWnd, dc);
}
return dpi / 96f;
}
private enum GetDeviceCapsIndex
{
LOGPIXELSX = 88,
LOGPIXELSY = 90
}
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(GetSystemMetricsIndex nIndex);
private enum GetSystemMetricsIndex
{
CXFRAME = 32,
CYFRAME = 33,
SM_CXPADDEDBORDER = 92
}
}
}
NativeMethods
using System;
using System.Runtime.InteropServices;
namespace WpfApp1
{
public static class NativeMethods
{
public const int WM_NCCALCSIZE = 0x83;
public const int WM_NCPAINT = 0x85;
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("dwmapi.dll", PreserveSig = false)]
public static extern bool DwmIsCompositionEnabled();
[DllImport("kernel32", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
public int leftWidth;
public int rightWidth;
public int topHeight;
public int bottomHeight;
}
private delegate int DwmExtendFrameIntoClientAreaDelegate(IntPtr hwnd, ref MARGINS margins);
public static int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins)
{
var hModule = LoadLibrary("dwmapi");
if (hModule == IntPtr.Zero)
{
return 0;
}
var procAddress = GetProcAddress(hModule, "DwmExtendFrameIntoClientArea");
if (procAddress == IntPtr.Zero)
{
return 0;
}
var delegateForFunctionPointer = (DwmExtendFrameIntoClientAreaDelegate)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(DwmExtendFrameIntoClientAreaDelegate));
return delegateForFunctionPointer(hwnd, ref margins);
}
public static bool IsDwmAvailable()
{
if (LoadLibrary("dwmapi") == IntPtr.Zero)
{
return false;
}
return true;
}
internal enum WVR
{
ALIGNTOP = 0x0010,
ALIGNLEFT = 0x0020,
ALIGNBOTTOM = 0x0040,
ALIGNRIGHT = 0x0080,
HREDRAW = 0x0100,
VREDRAW = 0x0200,
VALIDRECTS = 0x0400,
REDRAW = HREDRAW | VREDRAW
}
}
}

How to enable both scrolling and zooming using pinch in WPF?

I am struggling with making both touch events and manipulation work properly in a WPF project. I have a ScrollViewer which contains a picture and I would like to scroll both horizontally and vertically using a swipe gestures. Additionally, I would like to zoom in/out in the center of the pinch gesture. The code below achieves what I wish, but it has the following problems:
Sometimes the scrolling is laggy;
The scrolling does not work on the first try, only when attempting the same gesture a second time;
The zoom in/out does not work on the first try, only when attempting the same gesture a second time.
I enabled the IsManipulationEnabled and I implemented the code for zoom in/out functionality. However, I was not able to combine it with the scrolling functionality (by setting the PanningMode in the ScrollViewer only). Therefore, I created a custom control which inherits from Image control and I overwritten the OnTouchDown and OnTouchUp event handlers. Basically, what I am doing in these overwritten handlers is counting the number of touches on the screen and enabling/disabling manipulation. I also tried setting the PanningMode for the ScrollViewer, but it did not do the trick.
Below is the XAML:
<Grid>
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<local:CustomImage
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
ManipulationStarting="MainImage_ManipulationStarting"
ManipulationDelta="MainImage_ManipulationDelta">
</local:CustomImage>
</ScrollViewer>
</Grid>
Here is the code-behind:
public partial class MainWindow : Window
{
private void MainImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void MainImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
}
The XAML for the custom control:
<Style TargetType="{x:Type local:CustomImage}" />
Here is where I override the OnTouchDown and OnTouchUp event handlers:
public class CustomImage : Image
{
private volatile int nrOfTouchPoints;
private volatile bool isManipulationReset;
private object mutex = new object();
static CustomImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomImage), new FrameworkPropertyMetadata(typeof(CustomImage)));
}
protected override void OnTouchDown(TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
if (nrOfTouchPoints >= 2)
{
IsManipulationEnabled = true;
isManipulationReset = false;
}
}
base.OnTouchDown(e);
}
protected override void OnTouchUp(TouchEventArgs e)
{
lock (mutex)
{
if (!isManipulationReset)
{
IsManipulationEnabled = false;
isManipulationReset = true;
nrOfTouchPoints = 0;
}
}
base.OnTouchUp(e);
}
}
What I expect from this code is the following:
When using one finger to swipe horizontally or vertically across the touchscreen, the image should be scrolled accordingly;
When I use a pinch gesture on the touch screen, the image should be zoomed in/out in the center of the pinch.
Fortunately, I managed to find the perfect solution. Therefore, I am going to post the answer in the case that someone is working on a similar problem and needs some help.
What I did:
Got rid of the custom control as it was not necessary;
Create a field which counts the number of the touch points;
Implemented the TouchDown event handler, which increases the number of touch points by 1 (this method is called each time there is a touch down gesture on the device);
Implemented the TouchUp event handler, which decreases the number of touch points by 1 (this method is called each time there is a touch up gesture on the device);
In the Image_ManipulationDelta event handler, I check the number of touch points:
if the number of touch points < 2, then the translation value is added to the current offset of the scrollbars, thus achieving scrolling;
otherwise, the center of the pinch is calculated and a scale gesture is applied.
Here is the full XAML:
<Grid
x:Name="GridParent">
<ScrollViewer
x:Name="ScrollViewerParent"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
PanningMode="Both">
<Image
x:Name="MainImage"
Source="{Binding Source={x:Static local:Constants.ImagePath}}"
IsManipulationEnabled="True"
TouchDown="MainImage_TouchDown"
TouchUp="MainImage_TouchUp"
ManipulationDelta="Image_ManipulationDelta"
ManipulationStarting="Image_ManipulationStarting"/>
</ScrollViewer>
</Grid>
Here is the entire code discussed above:
public partial class MainWindow : Window
{
private volatile int nrOfTouchPoints;
private object mutex = new object();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
e.ManipulationContainer = ScrollViewerParent;
e.Handled = true;
}
private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
int nrOfPoints = 0;
lock (mutex)
{
nrOfPoints = nrOfTouchPoints;
}
if (nrOfPoints >= 2)
{
DataLogger.LogActionDescription($"Executed {nameof(Image_ManipulationDelta)}");
var matrix = MainImage.LayoutTransform.Value;
Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);
if (centerOfPinch == null)
{
return;
}
var deltaManipulation = e.DeltaManipulation;
matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
MainImage.LayoutTransform = new MatrixTransform(matrix);
Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);
double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;
double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;
double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;
ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);
e.Handled = true;
}
else
{
ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
}
}
private void MainImage_TouchDown(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints++;
}
}
private void MainImage_TouchUp(object sender, TouchEventArgs e)
{
lock (mutex)
{
nrOfTouchPoints--;
}
}
}
}

C# WPF Interupt DragMove with mouse event

I'm facing the following issue:
In WPF I have a window with WindowStyle="None" and so I add a button to move the window with the DragMove() method. This part is working fine. What I want in addition is that when the window reach a certain position it's stop the DragMove.
My idea was to make it by raising a MouseLeftButtonLeft thinking it will interrupt the DragMove but it isn't.
Button to move window:
<Button Grid.Column="0" x:Name="MoveButton" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="3" Cursor="Hand">
<Image x:Name="MoveImage" Source="images/move.png" MouseLeftButtonDown="MoveWindow" MouseLeftButtonUp="Poney" />
</Button>
Method to move the window:
// Move the window with drag of the button. Ensure that we are not over the taskbar
private void MoveWindow(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
DragMove();
// https://stackoverflow.com/questions/48399180/wpf-current-screen-dimensions
System.Drawing.Rectangle workingArea = Screen.FromHandle(new System.Windows.Interop.WindowInteropHelper(this).Handle).WorkingArea;
//The left property of the window is calculated on the width of all screens, so use VirtualScreenWidth to have the correct width
if (Top > (workingArea.Height - Height))
{
Top = (workingArea.Height - Height);
}
else if (Left > (SystemParameters.VirtualScreenWidth - Width))
{
Left = (SystemParameters.VirtualScreenWidth - Width);
}
else if (Left < 0)
{
Left = 0;
}
}
Method to raise event :
private void MainWindow_LocationChanged(object sender, EventArgs e)
{
// https://stackoverflow.com/questions/48399180/wpf-current-screen-dimensions
System.Drawing.Rectangle workingArea = Screen.FromHandle(new System.Windows.Interop.WindowInteropHelper(this).Handle).WorkingArea;
if(Left > 2000)
{
MouseButtonEventArgs mouseButtonEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left)
{
RoutedEvent = MouseLeftButtonUpEvent,
Source = MoveImage
};
MoveImage.RaiseEvent(mouseButtonEvent);
//InputManager.Current.ProcessInput(mouseButtonEvent);
}
}
Check that the event is raised :
public void Poney(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("Poney");
}
I have the 'poney' displaying in my console, so i guess the code to raise the event is working?
In short version I need a method to interrupt the dragmove so I can make some change and the restart DragMove.
Thanks :)
PS : The 2000 value is used for test, in "real" the position is calculated.
You could use the native mouse_event function to synthesize a mouse up event, e.g.:
const int MOUSEEVENTF_LEFTUP = 0x04;
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
void MainWindow_LocationChanged(object sender, EventArgs e)
{
if (Left > 1000)
{
Point point = Mouse.GetPosition(this);
mouse_event(MOUSEEVENTF_LEFTUP, (uint)point.X, (uint)point.Y, 0, 0);
}
}

Transparent window layer that is click-through and always stays on top

This is some code that I picked up which I tried to implement. Its purpose is to create a form layer which is transparent, full screen, borderless, clickthrough, and always on top of other windows. It then lets you draw using directx over the top of it remaining otherwise transparent.
The parts that don't work are the click-through part, and the directx render. When I run it I basically have an invisible force field in front of all other windows and have to alt-tab around to visual studio to quickly press ALT F5 and end the debug (so at least the always on top and transparency works). I have been trying to figure out why those parts don't work, but my newbie c# skills fail me. hopefully someone can spot why and provide a modification.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Globalization;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Threading;
namespace MinimapSpy
{
public partial class Form1 : Form
{
private Margins marg;
//this is used to specify the boundaries of the transparent area
internal struct Margins
{
public int Left, Right, Top, Bottom;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);
public const int GWL_EXSTYLE = -20;
public const int WS_EX_LAYERED = 0x80000;
public const int WS_EX_TRANSPARENT = 0x20;
public const int LWA_ALPHA = 0x2;
public const int LWA_COLORKEY = 0x1;
[DllImport("dwmapi.dll")]
static extern void DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMargins);
private Device device = null;
public Form1()
{
//Make the window's border completely transparant
SetWindowLong(this.Handle, GWL_EXSTYLE,
(IntPtr)(GetWindowLong(this.Handle, GWL_EXSTYLE) ^ WS_EX_LAYERED ^ WS_EX_TRANSPARENT));
//Set the Alpha on the Whole Window to 255 (solid)
SetLayeredWindowAttributes(this.Handle, 0, 255, LWA_ALPHA);
//Init DirectX
//This initializes the DirectX device. It needs to be done once.
//The alpha channel in the backbuffer is critical.
PresentParameters presentParameters = new PresentParameters();
presentParameters.Windowed = true;
presentParameters.SwapEffect = SwapEffect.Discard;
presentParameters.BackBufferFormat = Format.A8R8G8B8;
this.device = new Device(0, DeviceType.Hardware, this.Handle,
CreateFlags.HardwareVertexProcessing, presentParameters);
Thread dx = new Thread(new ThreadStart(this.dxThread));
dx.IsBackground = true;
dx.Start();
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
//Create a margin (the whole form)
marg.Left = 0;
marg.Top = 0;
marg.Right = this.Width;
marg.Bottom = this.Height;
//Expand the Aero Glass Effect Border to the WHOLE form.
// since we have already had the border invisible we now
// have a completely invisible window - apart from the DirectX
// renders NOT in black.
DwmExtendFrameIntoClientArea(this.Handle, ref marg);
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void dxThread()
{
while (true)
{
//Place your update logic here
device.Clear(ClearFlags.Target, Color.FromArgb(0, 0, 0, 0), 1.0f, 0);
device.RenderState.ZBufferEnable = false;
device.RenderState.Lighting = false;
device.RenderState.CullMode = Cull.None;
device.Transform.Projection = Matrix.OrthoOffCenterLH(0, this.Width, this.Height, 0, 0, 1);
device.BeginScene();
//Place your rendering logic here
device.EndScene();
//device.Present();
}
this.device.Dispose();
Application.Exit();
}
}
Here's a refined full sample code for making a window topmost - click through - transparent (= alpha blended). The sample makes a rotating color wheel which is rendered with DirectX, or actually with XNA 4.0, because I believe Microsoft has discontinued developing the managed directx and favours XNA today.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework.Graphics;
namespace ClickThroughXNA
{
public partial class Form1 : Form
{
// Directx graphics device
GraphicsDevice dev = null;
BasicEffect effect = null;
// Wheel vertexes
VertexPositionColor[] v = new VertexPositionColor[100];
// Wheel rotation
float rot = 0;
public Form1()
{
InitializeComponent();
StartPosition = FormStartPosition.CenterScreen;
Size = new System.Drawing.Size(500, 500);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; // no borders
TopMost = true; // make the form always on top
Visible = true; // Important! if this isn't set, then the form is not shown at all
// Set the form click-through
int initialStyle = GetWindowLong(this.Handle, -20);
SetWindowLong(this.Handle, -20, initialStyle | 0x80000 | 0x20);
// Create device presentation parameters
PresentationParameters p = new PresentationParameters();
p.IsFullScreen = false;
p.DeviceWindowHandle = this.Handle;
p.BackBufferFormat = SurfaceFormat.Vector4;
p.PresentationInterval = PresentInterval.One;
// Create XNA graphics device
dev = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, GraphicsProfile.Reach, p);
// Init basic effect
effect = new BasicEffect(dev);
// Extend aero glass style on form init
OnResize(null);
}
protected override void OnResize(EventArgs e)
{
int[] margins = new int[] { 0, 0, Width, Height };
// Extend aero glass style to whole form
DwmExtendFrameIntoClientArea(this.Handle, ref margins);
}
protected override void OnPaintBackground(PaintEventArgs e)
{
// do nothing here to stop window normal background painting
}
protected override void OnPaint(PaintEventArgs e)
{
// Clear device with fully transparent black
dev.Clear(new Microsoft.Xna.Framework.Color(0, 0, 0, 0.0f));
// Rotate wheel a bit
rot+=0.1f;
// Make the wheel vertexes and colors for vertexes
for (int i = 0; i < v.Length; i++)
{
if (i % 3 == 1)
v[i].Position = new Microsoft.Xna.Framework.Vector3((float)Math.Sin((i + rot) * (Math.PI * 2f / (float)v.Length)), (float)Math.Cos((i + rot) * (Math.PI * 2f / (float)v.Length)), 0);
else if (i % 3 == 2)
v[i].Position = new Microsoft.Xna.Framework.Vector3((float)Math.Sin((i + 2 + rot) * (Math.PI * 2f / (float)v.Length)), (float)Math.Cos((i + 2 + rot) * (Math.PI * 2f / (float)v.Length)), 0);
v[i].Color = new Microsoft.Xna.Framework.Color(1 - (i / (float)v.Length), i / (float)v.Length, 0, i / (float)v.Length);
}
// Enable position colored vertex rendering
effect.VertexColorEnabled = true;
foreach (EffectPass pass in effect.CurrentTechnique.Passes) pass.Apply();
// Draw the primitives (the wheel)
dev.DrawUserPrimitives(PrimitiveType.TriangleList, v, 0, v.Length / 3, VertexPositionColor.VertexDeclaration);
// Present the device contents into form
dev.Present();
// Redraw immediatily
Invalidate();
}
[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("dwmapi.dll")]
static extern void DwmExtendFrameIntoClientArea(IntPtr hWnd, ref int[] pMargins);
}
}
A little extension/modification to Jaska's code, which the form is transparent
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.TopMost = true; // make the form always on top
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; // hidden border
this.WindowState = FormWindowState.Maximized; // maximized
this.MinimizeBox = this.MaximizeBox = false; // not allowed to be minimized
this.MinimumSize = this.MaximumSize = this.Size; // not allowed to be resized
this.TransparencyKey = this.BackColor = Color.Red; // the color key to transparent, choose a color that you don't use
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
// Set the form click-through
cp.ExStyle |= 0x80000 /* WS_EX_LAYERED */ | 0x20 /* WS_EX_TRANSPARENT */;
return cp;
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// draw what you want
e.Graphics.FillEllipse(Brushes.Blue, 30, 30, 100, 100);
}
}
Change your extended window style to only WS_EX_LAYERED, window style to only WS_POPUP (NO WS_SIZEBOX) and make sure to use DwmExtendFrameIntoClientArea with all -1's and this will produce transparent windows with layered support: downside is you need to bltbit with GDI from an offscreen directx rendering. Not optimal but it works. This gives mouse click throughs + directx rendering + transparency. Downside is you'll need to inform GDI anytime, pull the directx buffer (all of it or just the damaged portions) and write them to the screem with bltbit.
Setting the extended window style to WS_EX_COMPOSITED and DwmExtendedFrameIntoClientArea with all -1's (similar as above, WS_POPUP on the regular window style). This you can run directx from but no mouse clickthroughs. You can at this point define irregular paths for the hit mask and pass it to windows, its not perfect but if you know a general (non regular) area that can pass-through it'll work.
Still trying to find a true way of using opengl/directx on mac or windows platforms that can pass through mouse clicks with out having to do a bitblt to a legacy rendering system.
I have a simple way use TransparentKey property and a 1x1 pixel label with the color of Form TransparentKey.
On Form and all control MouseMouse event. Set label position to Mouse location.
private void MoveHole()
{
var newLocation = PointToClient(MousePosition);
lblHole.Location = newLocation;
}

How to keep the aspect ratio of a UserControl?

Does anyone have an idea how to keep the Height/Width Ratio 1:1 of a UserControl?
E.g. if Height > Width, Width & Height will have the same size and vice versa.
I'm not sure this will work, but if you register a handler for the SizeChanged event and in there put in your code keep the aspect ratio 1:1.
The SizeChangedEventArgs argument has the old size and the new size so you can check which has changed and update the other accordingly.
You might need to introduce a guard variable so that you don't get a cascade of SizeChanged events as a result of updating the Height or Width.
Another alternative:
<local:MyControl Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"/>
Try using a ViewBox and setting its Stretch property to Uniform
i used this code for keeping aspect ratio
inside usercontrol globally define org_width, org_height, org_ratio :
private static double org_width = 77.6;//desired width
private static double org_height = 81.4;//desired height
private static double org_ratio = org_width / org_height;
use this code inside usercontrol in SizeChanged event:
FrameworkElement UCborder = this;
UCborder.Width = UCborder.Height*org_ratio;
and finally your user control code should looks like this:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace yournamespace
{
public partial class YourUserControl : UserControl
{
private static double org_width = 77.6;//desired width
private static double org_height = 81.4;//desired height
private static double org_ratio = org_width / org_height; // width/height
public YourUserControl()
{
InitializeComponent();
}
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement UCborder = this;
UCborder.Width = UCborder.Height*org_ratio;
}
}
}
good luck
private bool isSizeChangeDefered;
private void uiElement_SizeChanged(object sender, SizeChangedEventArgs e)
{
//Keep Acpect Ratio
const double factor = 1.8;
if(isSizeChangeDefered)
return;
isSizeChangeDefered = true;
try
{
if (e.WidthChanged)
{
driverPan.Height = e.NewSize.Width * factor;
}
if (e.HeightChanged)
{
driverPan.Height = e.NewSize.Width / factor;
}
}
finally
{
// e.Handled = true;
isSizeChangeDefered = false;
}
}
maybe this helps... cheers

Categories