I want to shrink the window so it fits on a screen.
Normally my window has size of 1000x800 so there is enough space in most scenarios but I'm also checking screen area to make sure it's not bigger. However my boss gave me his pc where he has screen scale set to 150% and the window becomes bigger then available space. I thought I solved this by dividing screen size by it's current dpi but turns out this does not work in all scenarios.
This is the method I'm using to set the size based on current DPI:
private void setMainWindowDimensions() {
//set window size and position with respect to current screen
var windowInteropHelper = new WindowInteropHelper(nativeWindow);
var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
var dpi = screen.getScale();
nativeWindow.Width = Math.Min(screen.WorkingArea.Width / dpi.x, 1000);
nativeWindow.Height = Math.Min(screen.WorkingArea.Height / dpi.y, 800);
}
And here is the getScale() extension method I'm using to get the scale factor:
//https://stackoverflow.com/questions/29438430/how-to-get-dpi-scale-for-all-screens
public static (uint x, uint y) GetDpi(this S screen, DpiType dpiType = default) {
var pnt = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
var mon = MonitorFromPoint(pnt, 2/*MONITOR_DEFAULTTONEAREST*/);
GetDpiForMonitor(mon, dpiType, out var dpiX, out var dpiY);
return (dpiX, dpiY);
}
//https://msdn.microsoft.com/en-us/library/windows/desktop/dd145062(v=vs.85).aspx
[DllImport("User32.dll")]
private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);
//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
[DllImport("Shcore.dll")]
private static extern IntPtr GetDpiForMonitor([In]IntPtr hmonitor, [In]DpiType dpiType, [Out]out uint dpiX, [Out]out uint dpiY);
This is is form the How to get DPI scale for all screens? answer.
Above code worked when I changed the scale of main monitor on my work pc, it correctly returned dpi of (1.5, 1.5). Few months later and my boss is back with he's computer saying it still don't work.
I figured out the DPI is taken form main screen, even if the window is opened on the other and here I still can't figure out how to get correct values.
I was following this article But it doesn't work at all. The methods form the NativeHelpers project are returning some values but the result is the same as with original method. Basically the SetPerMonitorDPIAware fails and I don't know why. I used slightly modified version and I thought that there is something wrong with my project configuration, but even when I create new WPF project, extending the window from PerMonitorDPIWindow class, the result is the same - it throws "Enabling Per-monitor DPI Failed."
private void createWindow() {
var c = logCtx("Creating main applicaton window", ONE_TIME_LOG);
var pma = PerMonitorDPIHelper.SetPerMonitorDPIAware();
if (pma == 0) log("Could not set per monitor DPI awareness.", WARNING_LOG);
else log("Per monitor awareness was set.", SUCCESS_LOG);
window = new CWindow();
nativeWindow.AllowDrop = true;
setMainWindowDimensions();
c.close();
}
private void setMainWindowDimensions() {
//set window size and position with respect to current screen
var windowInteropHelper = new WindowInteropHelper(nativeWindow);
var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
var dpi = screen.getScale();
var sdpi = PerMonitorDPIHelper.GetSystemDPI();
var wdpi = PerMonitorDPIHelper.GetDpiForWindow(windowInteropHelper.Handle);
log($#"DPI awareness: {PerMonitorDPIHelper.getPerMonitorDPIAware()}");
log($#"system DPI: {sdpi}, window DPI: {wdpi}, screen DPI: {dpi}");
nativeWindow.Width = Math.Min(screen.WorkingArea.Width / dpi.x, 1000);
nativeWindow.Height = Math.Min(screen.WorkingArea.Height / dpi.y, 800);
}
private void onLoaded(object sender, RoutedEventArgs e) {
var c = logCtx("Main window loaded", ONE_TIME_LOG);
setMainWindowDimensions();
c.close();
}
Here you can see returned DPI is 1.5 even if the window is opened on screen that is not scaled.
Since I have a drawing app, I'm also using the following method to determine real scale coordinates:
private (double x, double y) density {
get {
var m = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget
.TransformToDevice;
return (96 / m.M11 / 25.4, 96 / m.M22 / 25.4);
}
}
This solution is from this question, and it also don't work in various dpi scenarios.
I've also read in one of the comments that shcore is available only since Windows 8. Our PCs have Win 10 but we also want to support Windows 7.
There is also on more strange thing. My boss PC is actually a laptop with only one, main screen, yet it still doesn't get proper DPI. I don't have access to it right now so don't have logs I've just added...
Is there any method which works in all scenarios? It's seem that such such thing should require 2 lines of code yet it's became so convoluted I've already wasted 2 day on this and I'm out of ideas :(
Related
i'm developing VSTO add-in for outlook which includes overlay on top of the window.
I'm building my UI using WPF.
Problem is that when i'm trying to attach WPF Window ( merge left/top/width/height ) to outlook window when STARTING at scale more than 100% GetWindowsRect Returns wrong rectangle.
BUT when i'm starting application at 100% scale then change windows scale at runtime to whatever value everything is good and DPI Aware. Both cases ( starting and runtime ) GetDpiForWindow returns correct values which is...strange. DPI Awareness is set using SetThreadDpiAwareness when forms are created.
Can't get my head what's wrong :<. Any advises appreciated.
Code for attaching:
public void AttachTo(IntPtr src, AttachFlagEnum flags)
{
var nativeRectangle = new WinAPI.RECT();
if (!WinAPI.GetWindowRect(src, ref nativeRectangle))
{
// throw new Win32Exception(Marshal.GetLastWin32Error());
return;
}
AttachToCoords(new Rectangle(nativeRectangle.Left, nativeRectangle.Top, nativeRectangle.Right - nativeRectangle.Left, nativeRectangle.Bottom - nativeRectangle.Top), flags);
}
Form create code:
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
StateManager.Init();
OutlookUtils.WaitOutlookLoading();
using (var ctx = new DPIContextBlock(WinAPI.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
{
new Forms.One().Show();
new Forms.Overlay().Show();
new Forms.Two();
}
}
Overlay attach code (executes by timer )
private void OverlayThink(object ob)
{
if (Managers.StateManager.OutlookState == OutlookStateEnum.MINIMIZED || Managers.StateManager.UiState == UIStateEnum.DESCWND)
{
if (this.IsVisible)
{
this.Dispatcher.Invoke(() => this.Hide());
}
return;
}
this.Dispatcher.Invoke(() => this.AttachTo(Utils.OutlookUtils.GetWordWindow(), AttachFlagEnum.OVERLAY));
this.Dispatcher.Invoke(() => this.Show());
}
My solution to the problem was that AttachToCoords method sets coords from GetWindowRect directly to Window.Left | Right | Top | Bottom. That's wrong because internally WPF positions it's elements in 96 DPI coordinate system. So i was need to convert it before assigning.
Solution:
private Rectangle TransformCoords(Rectangle coords)
{
var source = PresentationSource.FromVisual(this);
coords.X = (int)(coords.X / source.CompositionTarget.TransformToDevice.M11);
coords.Y = (int)(coords.Y / source.CompositionTarget.TransformToDevice.M22);
coords.Width = (int)(coords.Width / source.CompositionTarget.TransformToDevice.M11);
coords.Height = (int)(coords.Height / source.CompositionTarget.TransformToDevice.M22);
return coords;
}
WPF (as well as Windows forms) should be scaled automatically depending on the DPI value set on the system. There is no need to calculate the size and positions of the dialog window in Outlook add-ins.
Instead, you need to set up the form correctly to follow the DPI settings and set the window parent, so it will be displayed on top of the Outlook window.
I am trying to create sort of a radar. Radar is VisualCollection that consists of 360 DrawingVisual's (which represent radar beams). Radar is placed on Viewbox.
class Radar : FrameworkElement
{
private VisualCollection visuals;
private Beam[] beams = new Beam[BEAM_POSITIONS_AMOUNT]; // all geometry calculation goes here
public Radar()
{
visuals = new VisualCollection(this);
for (int beamIndex = 0; beamIndex < BEAM_POSITIONS_AMOUNT; beamIndex++)
{
DrawingVisual dv = new DrawingVisual();
visuals.Add(dv);
using (DrawingContext dc = dv.RenderOpen())
{
dc.DrawGeometry(Brushes.Black, null, beams[beamIndex].Geometry);
}
}
DrawingVisual line = new DrawingVisual();
visuals.Add(line);
// DISCRETES_AMOUNT is about 500
this.Width = DISCRETES_AMOUNT * 2;
this.Height = DISCRETES_AMOUNT * 2;
}
public void Draw(int beamIndex, Brush brush)
{
using (DrawingContext dc = ((DrawingVisual)visuals[beamIndex]).RenderOpen())
{
dc.DrawGeometry(brush, null, beams[beamIndex].Geometry);
}
}
protected override Visual GetVisualChild(int index)
{
return visuals[index];
}
protected override int VisualChildrenCount
{
get { return visuals.Count; }
}
}
Each DrawingVisual has precalculated geometry for DrawingContext.DrawGeometry(brush, pen, geometry). Pen is null and brush is a LinearGradientBrush with about 500 GradientStops. The brush gets updated every few milliseconds, lets say 16 ms for this example. And that is what gets laggy. Here goes the overall logic.
In MainWindow() constructor I create the radar and start a background thread:
private Radar radar;
public MainWindow()
{
InitializeComponent();
radar = new Radar();
viewbox.Child = radar;
Thread t = new Thread(new ThreadStart(Run));
t.Start();
}
In Run() method there is an infinite loop, where random brush is generated, Dispatcher.Invoke() is called and a delay for 16 ms is set:
private int beamIndex = 0;
private Random r = new Random();
private const int turnsPerMinute = 20;
private static long delay = 60 / turnsPerMinute * 1000 / (360 / 2);
private long deltaDelay = delay;
public void Run()
{
int beginTime = Environment.TickCount;
while (true)
{
GradientStopCollection gsc = new GradientStopCollection(DISCRETES_AMOUNT);
for (int i = 1; i < Settings.DISCRETES_AMOUNT + 1; i++)
{
byte color = (byte)r.Next(255);
gsc.Add(new GradientStop(Color.FromArgb(255, 0, color, 0), (double)i / (double)DISCRETES_AMOUNT));
}
LinearGradientBrush lgb = new LinearGradientBrush(gsc);
lgb.StartPoint = Beam.GradientStarts[beamIndex];
lgb.EndPoint = Beam.GradientStops[beamIndex];
lgb.Freeze();
viewbox.Dispatcher.Invoke(new Action( () =>
{
radar.Draw(beamIndex, lgb);
}));
beamIndex++;
if (beamIndex >= BEAM_POSITIONS_AMOUNT)
{
beamIndex = 0;
}
while (Environment.TickCount - beginTime < delay) { }
delay += deltaDelay;
}
}
Every Invoke() call it performs one simple thing: dc.DrawGeometry(), which redraws the beam under current beamIndex. However, sometimes it seems, like before UI updates, radar.Draw() is called few times and instead of drawing 1 beam per 16 ms, it draws 2-4 beams per 32-64 ms. And it is disturbing. I really want to achieve smooth movement. I need one beam to get drawn per exact period of time. Not this random stuff. This is the list of what I have tried so far (nothing helped):
placing radar in Canvas;
using Task, BackgroundWorker, Timer, custom Microtimer.dll and setting different Thread Priorities;
using different ways of implementing delay: Environment.TickCount, DateTime.Now.Ticks, Stopwatch.ElapsedMilliseconds;
changing LinearGradientBrush to predefined SolidColorBrush;
using BeginInvoke() instead of Invoke() and changing Dispatcher Priorities;
using InvalidateVisuals() and ugly DoEvents();
using BitmapCache, WriteableBitmap and RenderTargetBitmap (using DrawingContext.DrawImage(bitmap);
working with 360 Polygon objects instead of 360 DrawingVisuals. This way I could avoid using Invoke() method. Polygon.FillProperty of each polygon was bound to ObservableCollection, and INotifyPropertyChanged was implemented. So simple line of code {brushCollection[beamIndex] = (new created and frozen brush)} led to polygon FillProperty update and UI was getting redrawn. But still no smooth movement;
probably there were few more little workarounds I could forget about.
What I did not try:
use tools to draw 3D (Viewport) to draw 2D radar;
...
So, this is it. I am begging for help.
EDIT: These lags are not about PC resources - without delay radar can do about 5 full circles per second (moving pretty fast). Most likely it is something about multithread/UI/Dispatcher or something else that I am yet to understand.
EDIT2: Attaching an .exe file so you could see what is actually going on: https://dl.dropboxusercontent.com/u/8761356/Radar.exe
EDIT3: DispatcherTimer(DispatcherPriority.Render) did not help aswell.
For smooth WPF animations you should make use of the
CompositionTarget.Rendering event.
No need for a thread or messing with the dispatcher. The event will automatically be fired before each new frame, similar to HTML's requestAnimationFrame().
In the event update your WPF scene and you're done!
There is a complete example available on MSDN.
You can check some graphics bottleneck using the WPF Performance Suite:
http://msdn.microsoft.com/es-es/library/aa969767(v=vs.110).aspx
Perforator is the tool that will show you performance issues. Maybe you are using a low performance VGA card?
while (Environment.TickCount - beginTime < delay) { }
delay += deltaDelay;
The sequence above blocks the thread. Use instead "await Task.Delay(...)" which doesn't block the thread like its counterpart Thread.Sleep(...).
I want to show my window on top of the TaskBar's clock when the windows starts.
How can I find the bottom right corner location of my desktop?
I use this code that works well in windows forms app but does not work correctly in WPF:
var desktopWorkingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
this.Left = desktopWorkingArea.Right - this.Width;
this.Top = desktopWorkingArea.Bottom - this.Height;
This code works for me in WPF both with Display 100% and 125%
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var desktopWorkingArea = System.Windows.SystemParameters.WorkArea;
this.Left = desktopWorkingArea.Right - this.Width;
this.Top = desktopWorkingArea.Bottom - this.Height;
}
In brief I use
System.Windows.SystemParameters.WorkArea
instead of
System.Windows.Forms.Screen.PrimaryScreen.WorkingArea
To access the desktop rectangle, you could use the Screen class - Screen.PrimaryScreen.WorkingArea property is the rectangle of your desktop.
Your WPF window has Top and Left properties as well as Width and Height, so you could set those properties relative to the desktop location.
You can use the window's SizeChanged event instead of Loaded if you want the window to stay in the corner when its size changes. This is especially handy if the window has Window.SizeToContent set to some value other than SizeToContent.Manual; in this case it will adjust to fit the content while staying in the corner.
public MyWindow()
{
SizeChanged += (o, e) =>
{
var r = SystemParameters.WorkArea;
Left = r.Right - ActualWidth;
Top = r.Bottom - ActualHeight;
};
InitializeComponent();
}
Note also that you should subtract ActualWidth and ActualHeight (instead of Width and Height as shown in some other replies) to handle more possible situations, for example switching between SizeToContent modes at runtime.
My code:
MainWindow.WindowStartupLocation = WindowStartupLocation.Manual;
MainWindow.Loaded += (s, a) =>
{
MainWindow.Height = SystemParameters.WorkArea.Height;
MainWindow.Width = SystemParameters.WorkArea.Width;
MainWindow.SetLeft(SystemParameters.WorkArea.Location.X);
MainWindow.SetTop(SystemParameters.WorkArea.Location.Y);
};
I solved this problem with a new window containing a label named MessageDisplay. The code accompanying the window was as follows:
public partial class StatusWindow : Window
{
static StatusWindow display;
public StatusWindow()
{
InitializeComponent();
}
static public void DisplayMessage( Window parent, string message )
{
if ( display != null )
ClearMessage();
display = new StatusWindow();
display.Top = parent.Top + 100;
display.Left = parent.Left + 10;
display.MessageDisplay.Content = message;
display.Show();
}
static public void ClearMessage()
{
display.Close();
display = null;
}
}
For my application, the setting of top and left puts this window below the menu on the main window (passed to DisplayMessage in the first parameter);
This above solutions did not entirely work for my window - it was too low and the bottom part of the window was beneath the taskbar and below the desktop workspace. I needed to set the position after the window content had been rendered:
private void Window_ContentRendered(object sender, EventArgs e)
{
var desktopWorkingArea = System.Windows.SystemParameters.WorkArea;
this.Left = desktopWorkingArea.Right - this.Width - 5;
this.Top = desktopWorkingArea.Bottom - this.Height - 5;
}
Also, part of the frame was out of view, so I had to adjust by 5. Not sure why this is needed in my situation.
#Klaus78 's answer is correct. But since this is first thing google pops up and if working in environments where screen resolution can change often such that your app runs on virtual desktops or virtual servers and you still need it to update its placement when the screen resolution changes I have found linking to the SystemEvents.DisplaySettingsChanged event to be beneficial. Here is an example using rx and you can put this in your constructor for your view.
Observable
.FromEventPattern<EventHandler, EventArgs>(_ => SystemEvents.DisplaySettingsChanged += _, _ => SystemEvents.DisplaySettingsChanged -= _)
.Select(_ => SystemParameters.WorkArea)
.Do(_ =>
{
Left = _.Right - Width;
Top = _.Bottom - Height;
})
.Subscribe();
In Win32 API, function SetWindowPos provided an easy way to move and resize window at once.
However, in WPF class Window doesn't have a method like SetWindowPos. So I must code like the following:
this.Left += e.HorizontalChange;
this.Top += e.VerticalChange;
this.Width = newWidth;
this.Height = newHeight;
Of course, it works well, but it's not simple. And it looks dirty.
How can i move a window and resize at once?
Is there an API?
I know you've already solved your problem, but I'll post a solution that I found in case it helps others.
Basically, You must declare that SetWindowsPos as an imported function from Win32 this is the signature
[DllImport("user32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags);
The function needs the hWnd of your window, in order to get it you can add an handler on the initialization of your windows (for example you could listen for the "SourceInitialized" event) and store that value in a private member of the class:
hwndSource = PresentationSource.FromVisual((Visual)sender) as HwndSource;
WPF manages device independent pixels, so you needs even a converter from dip to real pixel for your screen. This is done with these lines:
var source = PresentationSource.FromVisual(this);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;
Point[] p = new Point[] { new Point(this.Left + e.HorizontalChange, this.Top), new Point(this.Width - e.HorizontalChange, this.Height) };
transformToDevice.Transform(p);
Finally you can call SetWindowsPos:
SetWindowPos(this.hwndSource.Handle, IntPtr.Zero, Convert.ToInt32(p[0].X), Convert.ToInt32(p[0].Y), Convert.ToInt32(p[1].X), Convert.ToInt32(p[1].Y), SetWindowPosFlags.SWP_SHOWWINDOW);
Sources:
Win32 SetWindowPos
WPF Graphics Rendering
You could wrap your code in a helper method. Just like this:
public static class WindowExtensions {
public static void MoveAndResize( this Window value, double horizontalChange, double verticalChange, double width, double height ) {
value.Left += horizontalChange;
value.Top += verticalChange;
value.Width = width;
value.Height = height;
}
}
So your calling code looks like this:
this.MoveAndResize( 10, 10, 1024, 768 );
I've left off namespace and using declaration, keep that in mind when copying.
Edit:
You could also use the API. Personally I stick with the managed code unless I really need to use the API. But that is up to you.
i want to increase desktop size (programically), effect should be like attaching second monitor, on the primary monitor nothing should change after increase.
Such trick is needed to hide window off screen and then using PrintScreen get that window image, cutting it from whole screen.
P.s. PrintWindow() function wont help here, i want to capture somewhere hidden webcam preview image, which isn't overlay, but still "Activemovie Window" gives me black image.
If u dont believe me, try capturing hidden Windows Messanger preview screen, at tools->Audio tuning Wizard...
Hmm... You could write a video driver, that should do the trick.
(The reason why just moving the window offscreen doesn't work is because most programs don't re-paint the entire window - only the "damaged portion" - and even then, the device context might not remember the contents if it's drawn to video memory).
Here is a code in C# that made me be able to resize my form to way bigger than the desktop area defined by my 3 monitors. I used the DrawToBitmap after a random backgroundcolor setting to see if the nonvisible part of the window got painted, and it seems so. You just do the same in C to get the same result. Hooking etc if needed.
protected override void WndProc(ref Message m) {
if (m.ToString().Contains("GETMINMAXINFO")) {
//Get data
MINMAXINFO obj = (MINMAXINFO)Marshal.PtrToStructure(m.LParam, typeof(MINMAXINFO));
//Change
if (obj.ptMaxSize.X > 0) {
obj.ptMaxSize.X = 60000;
obj.ptMaxSize.Y = 60000;
obj.ptMaxTrackSize.X = 60000;
obj.ptMaxTrackSize.Y = 60000;
//Update
Marshal.StructureToPtr(obj, m.LParam, true);
}
}
if (m.ToString().Contains("WINDOWPOSCHANGING")) {
//Get data
WINDOWPOS obj = (WINDOWPOS)Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
//Change
if (obj.cx > 0) {
obj.cx = 8000;
//Update
Marshal.StructureToPtr(obj, m.LParam, true);
}
}
//Debug.WriteLine(m.ToString());
base.WndProc(ref m);
}
[StructLayout(LayoutKind.Sequential)]
internal struct MINMAXINFO {
internal POINT ptReserverd;
internal POINT ptMaxSize;
internal POINT ptMaxPosition;
internal POINT ptMinTrackSize;
internal POINT ptMaxTrackSize;
}
[StructLayout(LayoutKind.Sequential)]
internal struct POINT {
internal int X;
internal int Y;
}
[StructLayout(LayoutKind.Sequential)]
internal struct WINDOWPOS {
internal IntPtr hwnd;
internal IntPtr hWndInsertAfter;
internal int x;
internal int y;
internal int cx;
internal int cy;
internal uint flags;
}