Can I have a Checked state on a ToolStripSplitButton in WinForms? - c#

Is there a way to have a Checked state on a ToolStripSplitButton? I want to have a ToolStripButton with a Checked property to determine if an overlay is shown in my application's window. My users want to have direct interaction to control the opacity of the overlay, and to do that I imagine having a split button where the secondary button opens a slider bar to control the opacity. (That of course isn't standard functionality, but I'm confident of being able to put that together).
However, the ToolStripSplitButton doesn't have a Checked property, so I can't do that directly. I tried to have a standard ToolStripButton with a separate ToolStripSplitButton next to it, with the DropDownButtonWidth = 11, and Margin = -5, 1, 0, 2, so that it looks like it belongs to the ToolStripButton. It looks fine, but to complete the illusion of them being a single button, I need to get them to both to draw with the same VisualStyles.PushButtonState - so they both go Hot when the mouse is over either of them.
First I tried using the MouseEnter and MouseLeave events on both buttons to try to change the background color of the other button, but that had no effect. Then I tried to use the Paint event of each button to try to render them with the ButtonRenderer.DrawButton with a PushButtonState = Hot, but that didn't seem to be working either and was becoming very messy. It seems like there must be a better way!
Is there a simple, or at least a practical solution to this?
Edit: The effect I am after is something like this:

I suggest you to create your own UserControl and to use it as a ToolStripItem.
toolStripMenuItem.DropDownItems.Add(new ToolStripControlHost(new OverlayControl()));
Here is a simple example of the OverlayControl, which is deduced from your description (I suggest you to create a real UserControl with the Designer).
public class OverlayControl : UserControl
{
private readonly CheckBox _checkBox = new CheckBox();
private readonly TrackBar _trackBar = new TrackBar();
public ToolStripExtended()
{
_checkBox.Text = "Overlay";
BackColor = SystemColors.Window;
_checkBox.AutoSize = true;
_trackBar.AutoSize = true;
var flowLayoutPanel = new FlowLayoutPanel {AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink};
flowLayoutPanel.Controls.AddRange( new Control[] { _checkBox, _trackBar });
Controls.Add(flowLayoutPanel);
AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowAndShrink;
PerformLayout();
}
}

The solution that I came up with was to create my own button by inheriting from ToolStripSplitButton, add my own Checked property, and then manually draw over it when it is checked:
/// <summary>
/// ToolStripSplitCheckButton adds a Check property to a ToolStripSplitButton.
/// </summary>
public partial class ToolStripSplitCheckButton : ToolStripSplitButton
{
//==============================================================================
// Inner class: ToolBarButonSplitCheckButtonEventArgs
//==============================================================================
/// <summary>
/// The event args for the check button click event. To be able to use the OnCheckedChanged
/// event, we must also record a dummy button as well as this one (hack).
/// </summary>
public class ToolBarButonSplitCheckButtonEventArgs : ToolBarButtonClickEventArgs
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="split_button">The sender split check button</param>
public ToolBarButonSplitCheckButtonEventArgs(ToolStripSplitCheckButton split_button)
: base(new ToolBarButton("Dummy Button")) // Hack - Dummy Button is not used
{
SplitCheckButton = split_button;
}
/// <summary>
/// The ToolStripSplitCheckButton to be sent as an argument.
/// </summary>
public ToolStripSplitCheckButton SplitCheckButton { get; set; }
}
//==========================================================================
// Construction
public ToolStripSplitCheckButton()
{
m_checked = false;
m_mouse_over = false;
}
//==========================================================================
// Properties
/// <summary>
/// Indicates whether the button should toggle its Checked state on click.
/// </summary>
[Category("Behavior"),
Description("Indicates whether the item should toggle its selected state when clicked."),
DefaultValue(true)]
public bool CheckOnClick { get; set; }
/// <summary>
/// Indictates the Checked state of the button.
/// </summary>
[Category("Behavior"),
Description("Indicates whether the ToolStripSplitCheckButton is pressed in or not pressed in."),
DefaultValue(false)]
public bool Checked { get { return m_checked; } set { m_checked = value; } }
//==========================================================================
// Methods
/// <summary>
/// Toggle the click state on button click.
/// </summary>
protected override void OnButtonClick(EventArgs e)
{
if (CheckOnClick) {
m_checked = !m_checked;
// Raise the OnCheckStateChanged event when the button is clicked
if (OnCheckChanged != null) {
ToolBarButonSplitCheckButtonEventArgs args = new ToolBarButonSplitCheckButtonEventArgs(this);
OnCheckChanged(this, args);
}
}
base.OnButtonClick(e);
}
/// <summary>
/// On mouse enter, record that we are over the button.
/// </summary>
protected override void OnMouseEnter(EventArgs e)
{
m_mouse_over = true;
base.OnMouseEnter(e);
this.Invalidate();
}
/// <summary>
/// On mouse leave, record that we are no longer over the button.
/// </summary>
protected override void OnMouseLeave(EventArgs e)
{
m_mouse_over = false;
base.OnMouseLeave(e);
this.Invalidate();
}
/// <summary>
/// Paint the check highlight when required.
/// </summary>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (m_checked) {
// I can't get the check + mouse over to render properly, so just give the button a colour fill - Hack
if (m_mouse_over) {
using (Brush brush = new SolidBrush(Color.FromArgb(64, SystemColors.MenuHighlight))) {
e.Graphics.FillRectangle(brush, ButtonBounds);
}
}
ControlPaint.DrawBorder(
e.Graphics,
e.ClipRectangle, // To draw around the button + drop-down
//this.ButtonBounds, // To draw only around the button
SystemColors.MenuHighlight,
ButtonBorderStyle.Solid);
}
}
//==========================================================================
// Member Variables
// The delegate that acts as a signature for the function that is ultimately called
// when the OnCheckChanged event is triggered.
public delegate void SplitCheckButtonEventHandler(object source, EventArgs e);
public event SplitCheckButtonEventHandler OnCheckChanged;
private bool m_checked;
private bool m_mouse_over;
}
To use this, handle the OnCheckChanged event for this button.
This has a couple of clunky hacks, however. To be able to raise an OnCheckedChanged event for the check button, the event args have to include a reference to an unused dummy button in addition to the actual button. And even after experimenting with various renderers, I couldn't get the checked + mouse over render to be exactly the same as that for a normal check button, so I just draw a colour overlay, which is nearly right but not quite.

Related

Menu Item not showing in Xamarin.Forms after navigation back

I'm developing a Xamarin.Forms Android-App where on a ContentPage a CustomRenderer is being used to display a SearchView in the Header of the Page. The CustomRenderer for the "SearchPage" class looks like the following:
/// <summary>
/// The search page renderer.
/// </summary>
public class SearchPageRenderer : PageRenderer
{
/// <summary>
/// Gets or sets the search view.
/// </summary>
private SearchView searchView;
/// <summary>
/// Gets or sets the toolbar.
/// </summary>
private Toolbar toolbar;
/// <summary>
/// Reaction on the disposing of the page.
/// </summary>
/// <param name="disposing">A value indicating whether disposing.</param>
protected override void Dispose(bool disposing)
{
if (this.searchView != null)
{
this.searchView.QueryTextChange -= this.OnQueryTextChangeSearchView;
}
this.toolbar?.Menu?.RemoveItem(Resource.Menu.mainmenu);
base.Dispose(disposing);
}
/// <summary>
/// Reaction on the element changed event.
/// </summary>
/// <param name="e">The event argument.</param>
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
if (e?.NewElement == null || e.OldElement != null)
{
return;
}
this.AddSearchToToolBar();
}
/// <summary>
/// Adds a search item to the toolbar.
/// </summary>
private void AddSearchToToolBar()
{
this.toolbar = (CrossCurrentActivity.Current?.Activity as MainActivity)?.FindViewById<Toolbar>(Resource.Id.toolbar);
if (this.toolbar != null)
{
this.toolbar.Title = this.Element.Title;
this.toolbar.InflateMenu(Resource.Menu.mainmenu);
this.searchView = this.toolbar.Menu?.FindItem(Resource.Id.action_search)?.ActionView?.JavaCast<SearchView>();
if (this.searchView != null)
{
this.searchView.QueryTextChange += this.OnQueryTextChangeSearchView;
this.searchView.ImeOptions = (int)ImeAction.Search;
this.searchView.MaxWidth = int.MaxValue;
this.searchView.SetBackgroundResource(Resource.Drawable.textfield_search_holo_light);
}
}
}
/// <summary>
/// Reaction on the text change event of the searchbar.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event argument.</param>
private void OnQueryTextChangeSearchView(object sender, SearchView.QueryTextChangeEventArgs e)
{
var searchPage = this.Element as SearchPage;
searchPage?.SearchCommand?.Execute(e?.NewText);
}
}
Thanks to this Stack-Overflow Thread, i got it working like a charm so far.
Now, if the user taps on a item in the "SearchPage", a new ContentPage (called "DetailPage") is pushed to the NavigationStack using the following method:
private async Task PushPageAsync(object model, ContentPage page, INavigation navigation)
{
page.BindingContext = model;
await navigation.PushAsync(page).ConfigureAwait(false);
}
That works without problems. But if the users navigates from the "DetailPage" back to the "SearchPage" by using the Back-Button, the customized Search-Header isn't showing at all.
What I tried:
Using the "OnAppearing" event of the page instead of the "OnElementChanged". That didn't solve the problem at first sight. However, if I add a Task.Delay(500) to the OnAppearing-Method and then add the SearchView again, it is displayed. But this fix seems quite ugly, and if i sleep the app and resume while using the SearchPage, the Search-Widget is showing twice.
So my question is:
Is there a bug in Xamarin or am I doing something wrong?
Is there a bug in Xamarin or am I doing something wrong?
I can't say it's a bug, I prefer to consider it is as by design. The real problem is that you're trying to modify the Toolbar in your custom renderer, and based on your description, you're using NavigationPage, its NavigationPageRenderer will update the view of Toolbar each time when current page is changed.
So you're doing right to use the "OnAppearing" event of the page instead of the "OnElementChanged", which cause another problem, the updating of Toolbar is be delayed when the old page is removed as you can see from the source code in RemovePage method, while the OnAppearing method will be executed immediately when the new page is shown:
Device.StartTimer(TimeSpan.FromMilliseconds(10), () =>
{
UpdateToolbar();
return false;
});
So I don't think you're doing anything wrong too, the method you used await Task.Delay(500); can quickly solve this issue.
Based on your description here:
But this fix seems quite ugly, and if i sleep the app and resume while using the SearchPage, the Search-Widget is showing twice.
I suggest to change your SearchPage to a View, and then dynamically add/remove this view from pages:
public class SearchPage : View
{
...
}
and renderer for it (other codes are the same, only change to inherit from ViewRenderer and the parameter of OnElementChanged is different):
public class SearchPageRenderer : ViewRenderer
{
private SearchView searchView;
/// <summary>
/// Gets or sets the toolbar.
/// </summary>
private Android.Support.V7.Widget.Toolbar toolbar;
...
/// <summary>
/// Reaction on the element changed event.
/// </summary>
/// <param name="e">The event argument.</param>
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
if (e?.NewElement == null || e.OldElement != null)
{
return;
}
this.AddSearchToToolBar();
}
...
}
Then you can use it as a control/view in pages where you want to show it, not directly as a Page. For example, I want to show this search view in my MainPage, then navigate to MainPage in App.xaml.cs:
MainPage = new NavigationPage(new MainPage());
MainPage's layout:
<StackLayout>
...
</StackLayout>
At last override the OnAppearing and OnDisappearing method of the MainPage:
protected override async void OnAppearing()
{
base.OnAppearing();
await Task.Delay(500);
var content = this.Content as StackLayout;
content.Children.Add(new SearchPage());
}
protected override void OnDisappearing()
{
base.OnDisappearing();
var content = this.Content as StackLayout;
foreach (var child in content.Children)
{
var searchpage = child as SearchPage;
if (searchpage != null)
{
content.Children.Remove(searchpage);
return;
}
}
}
By the way, if you want to navigate from MainPage here to other pages, you can code like this:
this.Navigation.PushAsync(new Page1());
There is no need to create a PushPageAsync task for it.

Passing This(of my main form) to a class that handles direction change/and language change c#/Winforms

I am having trouble with this,
What i am trying to achieve is the following:
I have a main form which is the startup form (MDI Container)
and then a few forms that are the child forms, what i am trying to achieve is the following, this is code that is happening on every screen at the moment, and i want to build it into a class but, i don't know how to pass this (the click events happen on every form at the moment, and i want it to run of a menu click on the MDI Parent(container):
UPDATE
I have gotten it to work with all the ChildForms thanx #JamesBarras this is how my code works now, i have remove the button click events, for it is only running from a MenuStripItem_Click Event
#region Change Language
private void englishToolStripMenuItem_Click(object sender, EventArgs e) {
Class1 cls = new Class1();
/// This is for Main and all the forms of MainForms Children
foreach (var childForm in this.MdiChildren) {
cls.ChangeLanguage(sender, log, childForm, this, this.menuStrip, "en");
//ChangeLanguage("en", childForm);
}
}
private void arabicToolStripMenuItem_Click(object sender, EventArgs e) {
Class1 cls = new Class1();
/// This is for Main and all the forms of MainForms Children
foreach (var childForm in this.MdiChildren) {
cls.ChangeLanguage(sender, log, childForm, this, this.menuStrip, "ar");
//ChangeLanguage( "ar", childForm);
}
}
#endregion
I just created this class as this code was running on the same page as the click event as a Method, I want the class that i am using/created (ChangeLanguage) to know what this is,because on the normal form, i can say this but in a class it doesn't know what the this is on the form that i have my click events, here is my code in the class that i created:
UPDATE
I have fine tuned the Class to do exactly what i need it to do, yet, i still can't translate the MenuStripItems and i am getting the value of this but still no menustrip Translate only the direction switch works
namespace languageChange.Classes { class Class1 {
#region Methods
/// <summary>
/// This is mainly to change the language and the layout direction
/// </summary>
/// <param name="sender"></param>
/// <param name="log"></param>
/// <param name="form"></param>
/// <param name="thiss"></param>
/// <param name="strip"></param>
/// <param name="lang"></param>
public void ChangeLanguage(object sender, Logger log, Form form, Form thiss, Control strip, string lang) {
string senderText = sender.GetType().ToString();
RightToLeft direction = RightToLeft.No;
if (lang == "ar") {
direction = RightToLeft.Yes;
}
thiss.RightToLeft = direction;
CultureInfo CurrentLocale = new CultureInfo(lang);
Thread.CurrentThread.CurrentCulture = new CultureInfo(lang);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang);
foreach (Control childForm in form.Controls) {
ComponentResourceManager resources = new ComponentResourceManager(form.GetType());
resources.ApplyResources(childForm, "$this");
childForm.RightToLeft = direction;
if (log.isDebugEnabled) log.Debug("--------------------------------------> c = " + childForm.Name);
RefreshResources(log, lang, childForm, resources, CurrentLocale, strip);
}
}
/// <summary>
/// This is to for the Refresh all the Resources
/// </summary>
/// <param name="log"></param>
/// <param name="lang"></param>
/// <param name="ctrl"></param>
/// <param name="resources"></param>
/// <param name="CurrentLocale"></param>
/// <param name="strip"></param>
private static void RefreshResources(Logger log, string lang, Control ctrl, ComponentResourceManager resources, CultureInfo CurrentLocale, Control strip) {
ctrl.SuspendLayout();
resources.ApplyResources(ctrl, ctrl.Name, CurrentLocale);
foreach (Control control in ctrl.Controls) {
RefreshResources(log, lang, control, resources, CurrentLocale, strip); // recursion
if (strip is ToolStrip) {
RefreshResources(((ToolStrip)strip).Items, resources, CurrentLocale);
}
ctrl.ResumeLayout(true);
if (log.isTraceEnabled) log.Trace("c=" + ctrl.Name);
}
}
/// <summary>
/// Which is done here [Refer to previous Summary]
/// </summary>
/// <param name="col"></param>
/// <param name="resources"></param>
/// <param name="CurrentLocale"></param>
private static void RefreshResources(ToolStripItemCollection col, ComponentResourceManager resources, CultureInfo CurrentLocale) {
foreach (ToolStripMenuItem item in col) {
if (item is ToolStripMenuItem) {
RefreshResources(((ToolStripMenuItem)item).DropDownItems, resources, CurrentLocale);
}
resources.ApplyResources(item, item.Name, CurrentLocale);
}
}
#endregion }}
UPDATE
So i got everything to translate, and change direction except for the MenuStripItems and the Menustrip as well.
Please Help me as this is really important and i am struggling.
This is a Winforms application using Visual Studio 2013 Ultimate, with C#.
Don't mind the logger, it is something i just added to log the trace and debugger
Change the signature of ChangeLanguage(string lang) to ChangeLanguage(Form form, string lang) so that you can pass it the form you wish to alter.
subsequently you now need to refer to the form you are working on as the passed in one. So replace this in that method with form and the forms type typeof(Form1) with form.GetType()
Everywhere you want to change language on any for you can now call
Class1.ChangeLanguage(this, "en");
where this is the current form
To change language for an MDIParent you'll need to this:
private void englishToolStripMenuItem_Click(object sender, EventArgs e)
{
Class1.ChangeLanguage(log, this, "en");
foreach(var child in this.MdiChildren)
{
Class1.ChangeLanguage(log, child, "en");
}
}
Note that to change the language of the MenuStrip itself you'll need to add an overload of RefreshResources to cope with ToolStripItemCollections (they are component based not control based)
private static void RefreshResources(ToolStripItemCollection col, ComponentResourceManager resources, CultureInfo CurrentLocale)
{
foreach(ToolStripItem item in col)
{
if (item is ToolStripMenuItem)
{
RefreshResources(((ToolStripMenuItem)item).DropDownItems, resources, CurrentLocale);
}
resources.ApplyResources(item, item.Name, CurrentLocale);
}
}
and add the following code to the original RefreshResources
if (ctrl is ToolStrip)
{
RefreshResources(((ToolStrip)ctrl).Items, resources, CurrentLocale);
}

Dropdown like button on menu tab in Windows 7

I'm just curious if anybody has an idea on how to do the dropdown like button on the windows 7 explorer tab using WPF (See Screeshot below).
I use this class, and it looks great:
http://andyonwpf.blogspot.com/2006/10/dropdownbuttons-in-wpf.html
I will post here so it's cleaner to read:
/// <summary>
/// Andy On WPF: DropDownButtons in WPF
/// http://andyonwpf.blogspot.com/2006/10/dropdownbuttons-in-wpf.html
/// </summary>
public class DropDownButton : ToggleButton
{
#region Members
public enum Placement { Bottom, Right }
#endregion
#region Properties
#region DropDownPlacement
/// <summary>
/// DropDown placement.
/// </summary>
public Placement DropDownPlacement
{
get { return (Placement)GetValue(DropDownPlacementProperty); }
set { SetValue(DropDownPlacementProperty, value); }
}
/// <summary>
/// DropDown placement (Dependency Property).
/// </summary>
public static readonly DependencyProperty DropDownPlacementProperty =
DependencyProperty.Register("DropDownPlacement", typeof(Placement),
typeof(DropDownButton), new UIPropertyMetadata(null));
#endregion
#region DropDown
/// <summary>
/// DropDown property.
/// </summary>
public ContextMenu DropDown
{
get { return (ContextMenu)GetValue(DropDownProperty); }
set { SetValue(DropDownProperty, value); }
}
/// <summary>
/// DropDown property (Dependency property).
/// </summary>
public static readonly DependencyProperty DropDownProperty =
DependencyProperty.Register("DropDown", typeof(ContextMenu),
typeof(DropDownButton), new PropertyMetadata(null, OnDropDownChanged));
#endregion
#endregion
#region Events
private static void OnDropDownChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
((DropDownButton)sender).OnDropDownChanged(e);
}
void OnDropDownChanged(DependencyPropertyChangedEventArgs e)
{
if (DropDown != null)
{
DropDown.PlacementTarget = this;
switch (DropDownPlacement)
{
default:
case Placement.Bottom:
DropDown.Placement = PlacementMode.Bottom;
break;
case Placement.Right:
DropDown.Placement = PlacementMode.Right;
break;
}
this.Checked +=
new RoutedEventHandler((a, b) => { DropDown.IsOpen = true; });
this.Unchecked +=
new RoutedEventHandler((a, b) => { DropDown.IsOpen = false; });
DropDown.Closed +=
new RoutedEventHandler((a, b) => { this.IsChecked = false; });
}
}
#endregion
}
It has a custom control template, one that appears to have a transparent background unless on mouse-over, of course the gradient and border is different too.
On MSDN there is an example for a custom template which results in a rather blue button, basically you can do anything with the templates, their creation can be quite some work though. Expression Blend can be helpful for control templating.
To get the menu part of the dropdown you can set the button's ContextMenu property and then use the ContextMenu.Placement to position it correctly underneath the button.
You may also have to set ContextMenu.PlacementTarget to keep it relative to the Button.
Sorry for adding again, but I just realized this control also exists in the Extended WPF Toolkit on Codeplex:
http://wpftoolkit.codeplex.com/wikipage?title=DropDownButton
It implements like this (as per their site):
<extToolkit:DropDownButton Content="Click Me" Margin="15" >
<extToolkit:DropDownButton.DropDownContent>
<extToolkit:ColorCanvas />
</extToolkit:DropDownButton.DropDownContent>
</extToolkit:DropDownButton>
You can just add MenuItems in there as needed.
I have used this kit for other features (ChildWindow and SplitButton), and I think it's well done. Keep expecting more like this from CodePlex and Microsoft as people keep requesting more Office 2007 / 2010 functionality in WPF.

OnPaint not getting called if form doesn't have focus

I have a user control with custom painting. Constructor sets styles correctly, from what I can tell. Basic code:
public partial class LineChart2 : UserControl
{
public LineChart2()
{
InitializeComponent();
//Set control styles to eliminate flicker on redraw and to redraw on resize
this.SetStyle(
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.DoubleBuffer,
true);
SetDefaultValues();
}
protected override void OnPaint(PaintEventArgs e)
{
// breakpoint set here for verification
Paint~misc stuff(e.Graphics);
base.OnPaint(e);
}
private void UpdateGraph()
{
// this is called when the data that the control depends on changes
~update stuff();
this.Invalidate();
//this.Refresh();
}
}
The control is contained within a Panel on a standard WinForm.
I've tried both Invalidate and Refresh.
When using Invalidate(), the control will redraw properly as long as the form it is contained in has focus. Drawing is smooth. When I switch focus to another form, drawing ceases even though the events are still firing, and this.Invalidate() is still being called. The form is still fully visible on screen.
When using Refresh(), the control will redraw regardless of whether the form has focus, but the drawing constantly flickers, as if bypassing the double-buffering mechanism.
So how do I get the Invalidate message to properly invoke the OnPaint method regardless of focus?
Documentation says:
Calling the Invalidate method does not
force a synchronous paint; to force a
synchronous paint, call the Update
method after calling the Invalidate
method.
Have you tried calling Update after Invalidate?
You should not force the control do redraw (Update or Refresh) so often. The UI may get not responsive, others controls may not update, because you are giving all UI attention to the forced sync Refresh.
The right way is to draw only when UI is ready to do it. For that you need a render loop. The ApplicationLoopDoWork will be fired every time the UI is ready to draw something. The period depends on the machine speed and what is being redrawn.
The class is based on this post on Tom Miller's Blog.
Here is the class that I use to control that.
Make updates only on the ApplicationLoopDoWork call.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace Utilities.UI
{
/// <summary>
/// WinFormsAppIdleHandler implements a WinForms Render Loop (max FPS possible).
/// Reference: http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
/// </summary>
public sealed class WinFormsAppIdleHandler
{
private readonly object _completedEventLock = new object();
private event EventHandler _applicationLoopDoWork;
//PRIVATE Constructor
private WinFormsAppIdleHandler()
{
Enabled = false;
SleepTime = 10;
}
/// <summary>
/// Singleton from:
/// http://csharpindepth.com/Articles/General/Singleton.aspx
/// </summary>
private static readonly Lazy<WinFormsAppIdleHandler> lazy = new Lazy<WinFormsAppIdleHandler>(() => new WinFormsAppIdleHandler());
public static WinFormsAppIdleHandler Instance { get { return lazy.Value; } }
private bool _enabled = false;
/// <summary>
/// Gets or sets if must fire ApplicationLoopDoWork event.
/// </summary>
public bool Enabled
{
get { return _enabled; }
set {
if (value)
Application.Idle += Application_Idle;
else
Application.Idle -= Application_Idle;
_enabled = value;
}
}
/// <summary>
/// Gets or sets the minimum time betwen ApplicationLoopDoWork fires.
/// </summary>
public int SleepTime { get; set; }
/// <summary>
/// Fires while the UI is free to work. Sleeps for "SleepTime" ms.
/// </summary>
public event EventHandler ApplicationLoopDoWork
{
//Reason of using locks:
//http://stackoverflow.com/questions/1037811/c-thread-safe-events
add
{
lock (_completedEventLock)
_applicationLoopDoWork += value;
}
remove
{
lock (_completedEventLock)
_applicationLoopDoWork -= value;
}
}
/// <summary>
///Application idle loop.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Application_Idle(object sender, EventArgs e)
{
//Try to update interface
while (Enabled && IsAppIdle())
{
OnApplicationIdleDoWork(EventArgs.Empty);
//Give a break to the processor... :)
//8 ms -> 125 Hz
//10 ms -> 100 Hz
Thread.Sleep(SleepTime);
}
}
private void OnApplicationIdleDoWork(EventArgs e)
{
var handler = _applicationLoopDoWork;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Gets if the app is idle.
/// </summary>
/// <returns></returns>
public static bool IsAppIdle()
{
bool isIdle = false;
try
{
Message msg;
isIdle = !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
}
catch (Exception e)
{
//Should never get here... I hope...
MessageBox.Show("IsAppStillIdle() Exception. Message: " + e.Message);
}
return isIdle;
}
#region Unmanaged Get PeekMessage
// http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
[System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
#endregion
}
}
You also might try Invalidate(true) to trigger child controls to repaint as well.

OnMouseEnter for all controls on a form

I have OnMouseEnter and OnMouseLeave event handlers setup for my form. When the mouse moves over the form I want to set the opacity to 100% and when it moves away I want to set it to 25%. It works well, except when the mouse moves over one of the buttons on the form. The OnMouseLeave event fires and hides the form again. Is there a good way to handle this, without having to wire up OnMouseEnter for every control on the form?
EDIT: I'm going to leave this answer here, even though it can't be made to work reliably. The reason: to prevent somebody else from trying the same thing. See end of message for the reason it won't work.
You can do this fairly easily for the client rectangle by getting the cursor position and checking to see if it's within the Form's client area:
private void Form1_MouseLeave(object sender, EventArgs e)
{
Point clientPos = PointToClient(Cursor.Position);
if (!ClientRectangle.Contains(clientPos))
{
this.Opacity = 0.25;
}
}
This assumes that none of your child controls will be changing the opacity.
However, you'll find that it's a less than perfect solution, because when the mouse goes to the title bar, the Form goes to 0.25%. You could fix that by checking to see if the mouse position is within the window rect (using the Bounds property), but then your window will remain opaque if the mouse moves off the title bar and out of the window.
You have a similar problem when entering the title bar from outside.
I think you'll have to handle the WM_NCMOUSEENTER and WM_NCMOUSELEAVE messages in order to make this work reliably.
Why it can't work:
Even handling the non-client area notifications can fail. It's possible for the mouse to enter on a child control, which would prevent the Form from being notified.
I think it is impossible to do, without handling the MouseEnter and MouseLeave events of all the children, but you do not have to wire them manually.
Here is some code I copied & pasted from a project of mine. It does almost what you described here. I actually copied the idea and the framework from this site.
In the constructor I call the AttachMouseOnChildren() to attach the events.
The OnContainerEnter and OnContainerLeave are used to handle the mouse entering/leaving the form itself.
#region MouseEnter & Leave
private bool _childControlsAttached = false;
/// <summary>
/// Attach enter & leave events to child controls (recursive), this is needed for the ContainerEnter &
/// ContainerLeave methods.
/// </summary>
private void AttachMouseOnChildren() {
if (_childControlsAttached) {
return;
}
this.AttachMouseOnChildren(this.Controls);
_childControlsAttached = true;
}
/// <summary>
/// Attach the enter & leave events on a specific controls collection. The attachment
/// is recursive.
/// </summary>
/// <param name="controls">The collection of child controls</param>
private void AttachMouseOnChildren(System.Collections.IEnumerable controls) {
foreach (Control item in controls) {
item.MouseLeave += new EventHandler(item_MouseLeave);
item.MouseEnter += new EventHandler(item_MouseEnter);
this.AttachMouseOnChildren(item.Controls);
}
}
/// <summary>
/// Will be called by a MouseEnter event, with any of the controls within this
/// </summary>
void item_MouseEnter(object sender, EventArgs e) {
this.OnMouseEnter(e);
}
/// <summary>
/// Will be called by a MouseLeave event, with any of the controls within this
/// </summary>
void item_MouseLeave(object sender, EventArgs e) {
this.OnMouseLeave(e);
}
/// <summary>
/// Flag if the mouse is "entered" in this control, or any of its children
/// </summary>
private bool _containsMouse = false;
/// <summary>
/// Is called when the mouse entered the Form, or any of its children without entering
/// the form itself first.
/// </summary>
protected void OnContainerEnter(EventArgs e) {
// No longer transparent
this.Opacity = 1;
}
/// <summary>
/// Is called when the mouse leaves the form. When the mouse leaves the form via one of
/// its children, this will also call OnContainerLeave
/// </summary>
/// <param name="e"></param>
protected void OnContainerLeave(EventArgs e) {
this.Opacity = DEFAULT_OPACITY;
}
/// <summary>
/// <para>Is called when a MouseLeave occurs on this form, or any of its children</para>
/// <para>Calculates if OnContainerLeave should be called</para>
/// </summary>
protected override void OnMouseLeave(EventArgs e) {
Point clientMouse = PointToClient(Control.MousePosition);
if (!ClientRectangle.Contains(clientMouse)) {
this._containsMouse = false;
OnContainerLeave(e);
}
}
/// <summary>
/// <para>Is called when a MouseEnter occurs on this form, or any of its children</para>
/// <para>Calculates if OnContainerEnter should be called</para>
/// </summary>
protected override void OnMouseEnter(EventArgs e) {
if (!this._containsMouse) {
_containsMouse = true;
OnContainerEnter(e);
}
}
#endregion
I think one way to reliably handle the mouse events you're interested is to set up an IMessageFilter on your Application object from which you can intercept all mouse messages (WM_MOUSEMOVE etc ..) even if they are sent to child controls of the form.
Here's some demo code:
using System;
using System.Windows.Forms;
namespace Test
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
public static Form frm = null;
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
frm = new Form1 {Opacity = 0.25};
frm.Controls.Add(new Button{Dock = DockStyle.Fill, Text = "Ok"});
Application.AddMessageFilter(new MouseMoveFilter());
Application.Run(frm);
}
}
public class MouseMoveFilter : IMessageFilter
{
#region IMessageFilter Members
private const int WM_MOUSELEAVE = 0x02A3;
private const int WM_NCMOUSEMOVE = 0x0A0;
private const int WM_MOUSEMOVE = 0x0200;
private const int WM_NCMOUSELEAVE = 0x2A2;
public bool PreFilterMessage(ref Message m)
{
switch (m.Msg)
{
case WM_NCMOUSEMOVE:
case WM_MOUSEMOVE:
Program.frm.Opacity = 1;
break;
case WM_NCMOUSELEAVE:
case WM_MOUSELEAVE:
if (!Program.frm.Bounds.Contains(Control.MousePosition))
Program.frm.Opacity = 0.25;
break;
}
return false;
}
#endregion
}
}
Alternatively you can inherit from Form class and override PreProcessMessage() to accomplish the same thing ...

Categories