I'm creating a custom drawn window by specifying border style NONE and custom processing of WM_NCHITTEST. I've defined some area as 'my window caption' and returning HTCAPTION result for WM_NCHITTEST in this area. When window is in normal state the behavior is expected by me. The window can be moved by dragging 'my window caption' and can be maximized by double clicking on it.
The problem is with the behavior of my window in maximized state. I still returning HTCAPTION result for WM_NCHITTEST in area of 'my window caption' and window can be restored to original size by double clicking on it again, but it's also still can be moved and this isn't what I want. What should I do to fix such behavior?
Fix:
protected override void WndProc(ref Message m)
{
if(m.Msg == WM_NCHITTEST)
{
Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
pos = this.PointToClient(pos);
if(HitTestForNC(ref m, pos))
{
if(WindowState != FormWindowState.Maximized || m.Result != (IntPtr)HitTestValues.HTCAPTION)
{
return;
}
}
}
else if(m.Msg == WM_GETMINMAXINFO)
{
base.WndProc(ref m);
MinMaxInfo mmi = (MinMaxInfo)Marshal.PtrToStructure(m.LParam, typeof(MinMaxInfo));
mmi.ptMaxPosition = Screen.FromControl(this).WorkingArea.Location;
mmi.ptMaxSize = Screen.FromControl(this).WorkingArea.Size;
Marshal.StructureToPtr(mmi, m.LParam, false);
return;
}
base.WndProc(ref m);
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
if(e.Button == MouseButtons.Left)
{
Message m = new Message();
if(HitTestForNC(ref m, e.Location))
{
if(m.Result == (IntPtr)HitTestValues.HTCAPTION && WindowState == FormWindowState.Maximized)
{
WindowState = FormWindowState.Normal;
return;
}
}
}
base.OnMouseDoubleClick(e);
}
HitTestForNC method is responsible for evaluation of hit test result on my custom drawn form. The implementation may look ugly, but it's pretty simple.
This is the code I use to prevent the window from being draggable when maximized.
// Indicates the form caption
Const HT_CAPTION As Integer = &H2
// Windows Message Non Client Button Down
Const WM_NCLBUTTONDOWN As Integer = &HA1
//Routine to implement the 'Drag Window' functionality.
Private Sub frm_Drag(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles Me.MouseDown
//Do not move the form if it is maximized.
If Not Me.WindowState = FormWindowState.Maximized Then
If e.Button = Windows.Forms.MouseButtons.Left Then
sender.Capture = False
Me.WndProc(Message.Create(Me.Handle, WM_NCLBUTTONDOWN, _
CType(HT_CAPTION, IntPtr), IntPtr.Zero))
End If
End If
End Sub
What you describe is a bit weird - the maximized window can't usually be moved. In particular it occupies the whole desktop area, and AFAIK the system doesn't 'drag' it when its caption (i.e. - the area for which hit test was HT_CAPTION) is dragged.
Can you please specify more info:
How many desktops do you have (is it a multi-monitor system)?
Do you respond on WM_GETMINMAXINFO to prevent your window from occupying the whole desktop area?
BTW I can imagine a workaround: when your window is maximized - don't return HT_CAPTION on hit test. Instead you may return HT_CLIENT, this will prevent your window from dragging.
However you'll have to manually implement "restoring" of your window when double-clicked. You should then respond on WM_LBUTTONDBLCLK and restore your position manually.
From Windows 7 the expected and correct behaviour is that maximized windows that are dragged should be draggable. Try it with Notepad or any other windows app:- Windows that are docked or maximized will automatically revert to the "restore" size and be draggable.
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 have a control that is similar to a Popup or Menu. I want to display it and when the user clicks outside the bounds of the box, have it hide itself. I've used Mouse.Capture(this, CaptureMode.SubTree) as well as re-acquired the capture the same way Menu/Popup do in OnLostMouseCapture.
When the user clicks outside the bounds of the control, I release the mouse capture in OnPreviewMouseDown. I don't set e.Handled to true. The mouse click will make it to other controls on the main UI, but not to the close button (Red X) for the window. It requires 2 clicks to close the app.
Is there a way to tell WPF to restart the mouse click, or to send a repeated mouse click event?
Here's my code. Note I renamed it to MainMenuControl - I'm not building a Menu, so Menu/MenuItem and Popup aren't options.
public class MainMenuControl : Control
{
static MainMenuControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MainMenuControl), new FrameworkPropertyMetadata(typeof(MainMenuControl)));
}
public MainMenuControl()
{
this.Loaded += new RoutedEventHandler(MainMenuControl_Loaded);
Mouse.AddPreviewMouseDownOutsideCapturedElementHandler(this, OnPreviewMouseDownOutsideCapturedElementHandler);
}
void MainMenuControl_Loaded(object sender, RoutedEventArgs e)
{
this.IsVisibleChanged += new DependencyPropertyChangedEventHandler(MainMenuControl_IsVisibleChanged);
}
void MainMenuControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.IsVisible)
{
Mouse.Capture(this, CaptureMode.SubTree);
Debug.WriteLine("Mouse.Capture");
}
}
// I was doing this in OnPreviewMouseDown, but changing to this didn't have any effect
private void OnPreviewMouseDownOutsideCapturedElementHandler(object sender, MouseButtonEventArgs e)
{
Debug.WriteLine("OnPreviewMouseDownOutsideCapturedElementHandler");
if (!this.IsMouseInBounds())
{
if (Mouse.Captured == this)
{
Mouse.Capture(this, CaptureMode.None);
Debug.WriteLine("Mouse.Capture released");
}
Debug.WriteLine("Close Menu");
}
}
protected override void OnLostMouseCapture(MouseEventArgs e)
{
base.OnLostMouseCapture(e);
Debug.WriteLine("OnLostMouseCapture");
MainMenuControl reference = e.Source as MainMenuControl;
if (Mouse.Captured != reference)
{
if (e.OriginalSource == reference)
{
if ((Mouse.Captured == null) || (!reference.IsAncestorOf(Mouse.Captured as DependencyObject)))
{
//TODO: Close
Debug.WriteLine("Close Menu");
}
}
// if a child caused use to lose the capture, then recapture.
else if (reference.IsAncestorOf(e.OriginalSource as DependencyObject))
{
if (Mouse.Captured == null)
{
Mouse.Capture(reference, CaptureMode.SubTree);
Debug.WriteLine("Mouse.Capture");
e.Handled = true;
}
}
else
{
//TODO: Close
Debug.WriteLine("Close Menu");
}
}
}
private bool IsMouseInBounds()
{
Point point = Mouse.GetPosition(this);
Rect bounds = new Rect(0, 0, this.Width, this.Height);
return bounds.Contains(point);
}
}
The problem is that the mouse handling you are talking about is outside the WPF eventing system and part of the operating system so we're really talking about two fairly different mouse message queues that interact well enough most of the time but in these edge case we see that the interoperability is not perfect.
You could try to generate Win32 mouse messages or send your own window a close message but all those approaches are hacks. Since popups and menus exhibit exactly the same symptoms you describe, it doesn't seem like there is going to be an easy to way to accomplish what you want as you've described it.
Instead, I suggest that you consider giving up the mouse capture when the mouse leaves the north client area of the window or some other heuristic such as a specified distance from the control. I know this is probably not ideal but it might be a satisfactory compromise if you want the close button to work badly enough.
Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".
I have a Window set to the height and width of my monitors:
var r = System.Drawing.Rectangle.Union( System.Windows.Forms.Screen.AllScreens[0].Bounds, System.Windows.Forms.Screen.AllScreens[1].Bounds );
Height = r.Height;
Width = r.Width;
This is all fine until I Lock my computer (WIN+L), when I come back the window has resized itself to be on one monitor only.
What I want to do is prevent the decrease in size, as I'm drawing on a canvas on the second monitor, and when the resize occurs, this is all lost..
Any thoughts on how I can prevent this?
Cheers!
You can use the Unlock/Lock event in .NET. Store your window height, width and position during the lock event and restore it on an Unlock event. Make sure you add "using Microsoft.Win32"
SystemEvents.SessionSwitch += new SessionSwitchEventHandler(SystemEvents_SessionSwitch);
private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
if (e.Reason == SessionSwitchReason.SessionUnlock)
{
//Put resize logic here
}
else if (e.Reason == SessionSwitchReason.SessionLock)
{
//Put size store logic here
}
}
For a System.Windows.Forms.TextBox with Multiline=True, I'd like to only show the scrollbars when the text doesn't fit.
This is a readonly textbox used only for display. It's a TextBox so that users can copy the text out. Is there anything built-in to support auto show of scrollbars? If not, should I be using a different control? Or do I need to hook TextChanged and manually check for overflow (if so, how to tell if the text fits?)
Not having any luck with various combinations of WordWrap and Scrollbars settings. I'd like to have no scrollbars initially and have each appear dynamically only if the text doesn't fit in the given direction.
#nobugz, thanks, that works when WordWrap is disabled. I'd prefer not to disable wordwrap, but it's the lesser of two evils.
#André Neves, good point, and I would go that way if it was user-editable. I agree that consistency is the cardinal rule for UI intuitiveness.
I came across this question when I wanted to solve the same problem.
The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.
Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.
using System;
using System.Drawing;
using System.Windows.Forms;
public class MyTextBox : TextBox {
private bool mScrollbars;
public MyTextBox() {
this.Multiline = true;
this.ReadOnly = true;
}
private void checkForScrollbars() {
bool scroll = false;
int cnt = this.Lines.Length;
if (cnt > 1) {
int pos0 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(0)).Y;
if (pos0 >= 32768) pos0 -= 65536;
int pos1 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(1)).Y;
if (pos1 >= 32768) pos1 -= 65536;
int h = pos1 - pos0;
scroll = cnt * h > (this.ClientSize.Height - 6); // 6 = padding
}
if (scroll != mScrollbars) {
mScrollbars = scroll;
this.ScrollBars = scroll ? ScrollBars.Vertical : ScrollBars.None;
}
}
protected override void OnTextChanged(EventArgs e) {
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnClientSizeChanged(EventArgs e) {
checkForScrollbars();
base.OnClientSizeChanged(e);
}
}
I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.
I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!
Perhaps it would be a good idea just to let your application follow Windows' look and feel.
There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.
Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.
So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.
I had some success with the code below.
public partial class MyTextBox : TextBox
{
private bool mShowScrollBar = false;
public MyTextBox()
{
InitializeComponent();
checkForScrollbars();
}
private void checkForScrollbars()
{
bool showScrollBar = false;
int padding = (this.BorderStyle == BorderStyle.Fixed3D) ? 14 : 10;
using (Graphics g = this.CreateGraphics())
{
// Calcualte the size of the text area.
SizeF textArea = g.MeasureString(this.Text,
this.Font,
this.Bounds.Width - padding);
if (this.Text.EndsWith(Environment.NewLine))
{
// Include the height of a trailing new line in the height calculation
textArea.Height += g.MeasureString("A", this.Font).Height;
}
// Show the vertical ScrollBar if the text area
// is taller than the control.
showScrollBar = (Math.Ceiling(textArea.Height) >= (this.Bounds.Height - padding));
if (showScrollBar != mShowScrollBar)
{
mShowScrollBar = showScrollBar;
this.ScrollBars = showScrollBar ? ScrollBars.Vertical : ScrollBars.None;
}
}
}
protected override void OnTextChanged(EventArgs e)
{
checkForScrollbars();
base.OnTextChanged(e);
}
protected override void OnResize(EventArgs e)
{
checkForScrollbars();
base.OnResize(e);
}
}
What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.
For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.
I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.
The following seems to work pretty well, with or without a border, and it works with WordWrap on.
Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.
public class TextBoxAutoScroll : TextBox
{
public void AutoScrollVertically(bool recalculateOnResize = false)
{
SuspendLayout();
if (recalculateOnResize)
{
Resize -= OnResize;
Resize += OnResize;
}
float linesHeight = 0;
var borderStyle = BorderStyle;
BorderStyle = BorderStyle.None;
int textHeight = PreferredHeight;
try
{
using (var graphics = CreateGraphics())
{
foreach (var text in Lines)
{
var textArea = graphics.MeasureString(text, Font);
if (textArea.Width < Width)
linesHeight += textHeight;
else
{
var numLines = (float)Math.Ceiling(textArea.Width / Width);
linesHeight += textHeight * numLines;
}
}
}
if (linesHeight > Height)
ScrollBars = ScrollBars.Vertical;
else
ScrollBars = ScrollBars.None;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
BorderStyle = borderStyle;
ResumeLayout();
}
}
private void OnResize(object sender, EventArgs e)
{
m_timerResize.Stop();
m_timerResize.Tick -= OnDelayedResize;
m_timerResize.Tick += OnDelayedResize;
m_timerResize.Interval = 475;
m_timerResize.Start();
}
Timer m_timerResize = new Timer();
private void OnDelayedResize(object sender, EventArgs e)
{
m_timerResize.Stop();
Resize -= OnResize;
AutoScrollVertically();
Resize += OnResize;
}
}