Problem
A UserControl raises by default the Click Event when a mouse button is pressed. Is there a way to prevent the event from raising when clicking on a specific area of the UserControl?
Code
Suppose that we have this specific UserControl. There is a red rectangle: I want to raise the click event only in the white area.
I've tried to override the OnMouseClick sub:
Public Class UserControl1
Dim noClickArea As New Rectangle(100, 100, 50, 50)
'Draw the red rectangle
Private Sub UserControl1_Paint(sender As Object, e As PaintEventArgs) Handles MyBase.Paint
Using gr As Graphics = Me.CreateGraphics
gr.Clear(Color.White)
gr.FillRectangle(Brushes.Red, noClickArea)
End Using
End Sub
'This aims to prevent the mouseClick event on noClickArea
Protected Overrides Sub OnMouseClick(e As MouseEventArgs)
If Not noClickArea.Contains(e.Location) Then
MyBase.OnMouseClick(e)
End If
End Sub
End Class
Here is a test Form that use this UserControl and show a message box on click:
Public Class Form1
Private Sub UserControl11_Click(sender As Object, e As EventArgs) Handles UserControl11.Click
MessageBox.Show("Click")
End Sub
End Class
Result
Overriding OnMouseClick don't produce the expected result:
How can I achieve the result I want?
You're overriding OnMouseClick, but you have subscribed to the Click event in your Form.
OnMouseClick is called after OnClick, so you suppress MouseClick, but the Click event has already been raised.
Override OnClick instead. Or subscribe to the MouseClick event in your Form.
Note that EventArgs of the OnClick method is actually a MouseEventArgs object, so you can cast EventArgs to MouseEventArgs:
Protected Overrides Sub OnClick(e As EventArgs)
If noClickArea.Contains(CType(e, MouseEventArgs).Location) Then Return
MyBase.OnClick(e)
End Sub
Then, override OnPaint instead of subscribing to the Paint event in your UserControl.
Also, do not use Me.CreateGraphics in that event or method override, it doesn't make sense.
Use the Graphics object provided by the PaintVentArgs:
Protected Overrides Sub OnPaint(e As PaintEventArgs)
e.Graphics.Clear(Color.White)
e.Graphics.FillRectangle(Brushes.Red, noClickArea)
MyBase.OnPaint(e)
End Sub
As a suggestion (since I don't know what is the real use case), if you want to fill the whole Control's background with a color, set the Background Color of the Control in the Designer instead of using Graphics.Clear() in that context.
Related
I am trying to create a VB.net form where the user can click on a button and it is associated with a specific image and when the user clicks the image and then clicks select it will add it to a variable.
If you need more information please let me know (this is the image of my form)
I wanted to know:
how to assign an image to a button ( so when you click it, it means that the user wants that image) I am not sure if my method is correct
how to assign the image that was selected to a variable (so that once the variable is called it will output the image)
and how to end the form once the image is selected. (once use clicks the select button it should end the form, or if the user clicks cancel it should end the form).
Public Class Form1
Private Property SelectedPictureBox() As Image
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Controls.OfType(Of ButtonPictureBox).ToList().ForEach(
Sub(box)
AddHandler box.Click, AddressOf AllButton_Click
End Sub)
End Sub
Private Sub AllButton_Click(sender As Object, e As EventArgs)
SelectedPictureBox = CType(sender, ButtonPictureBox).PictureBox
End Sub
Private Sub GetBtnSelect_Click(sender As Object, e As EventArgs) Handles GetBtnSelect.Click
If SelectedPictureBox IsNot Nothing Then
MessageBox.Show(SelectedPictureBox.Name)
Else
MessageBox.Show("Please select an image")
End If
End Sub
Private Sub BtnTropicalFloral_Click(sender As Object, e As EventArgs) Handles BtnTropicalFloral.Click
SelectedPictureBox = PictureBoxTropicalFloral.Image
End Sub
Private Sub BtnLightGeode_Click(sender As Object, e As EventArgs) Handles BtnLightGeode.Click
SelectedPictureBox = PictureBoxLightGeode.Image
End Sub
Private Sub BtnStripes_Click(sender As Object, e As EventArgs) Handles BtnStripes.Click
SelectedPictureBox = PictureBoxStripes.Image
End Sub
Private Sub BtnAuroraBorealis_Click(sender As Object, e As EventArgs) Handles BtnAuroraBorealis.Click
SelectedPictureBox = PictureBoxAuroraBorealis.Image
End Sub
Private Sub BtnDiagonals_Click(sender As Object, e As EventArgs) Handles BtnDiagonals.Click
SelectedPictureBox = PictureBoxDiagonals.Image
End Sub
Private Sub BtnComb_Click(sender As Object, e As EventArgs) Handles BtnComb.Click
SelectedPictureBox = PictureBoxComb.Image
End Sub
Private Sub BtnMountain_Click(sender As Object, e As EventArgs) Handles BtnMountain.Click
SelectedPictureBox = PictureBoxMountain.Image
End Sub
Private Sub BtnLandscape_Click(sender As Object, e As EventArgs) Handles BtnLandscape.Click
SelectedPictureBox = PictureBoxLandscape.Image
End Sub
Private Sub BtnGradient_Click(sender As Object, e As EventArgs) Handles BtnGradient.Click
SelectedPictureBox = PictureBoxGradient.Image
End Sub
Private Sub BtnAbstract_Click(sender As Object, e As EventArgs) Handles BtnAbstract.Click
SelectedPictureBox = PictureBoxAbstract.Image
End Sub
Private Sub BtnGeode_Click(sender As Object, e As EventArgs) Handles BtnGeode.Click
SelectedPictureBox = PictureBoxGeode.Image
End Sub
End Class
My VB is a bit rusty, so I'll have to give the answer in C#. I'm sure you'll get the gist, and will be able to translate it into VB.
First of all: whenever you see that you are repeating things, for instance by copy-paste, you should stop, and rethink your design: maybe it would be better to create a method for this, or an object.
All your PictureBox/Button combinations have some common behavior. This means that you should think of creating a class for it.
In your case, you have the combination of a Button and a PictureBox. You want to be able to set and get an Image. The PictureBox will show this image. Finally you want to be notified if the operator clicks the Button.
In the event handler of this Notification you want to know which pictureBox/Button combination sent the event. You get the image from this combination and close the form.
In WinForms, whenever you want to have a class that is a combination of several controls, you make a UserControl.
In Visual Studio, in the solution control, right click on the project (not the solution!) and select: Add new Item. Select to add a new User Control. Think of a proper name, I'll call this ImageSelectionControl
Use the visual studio designer to give ImageSelectionControl the proper size. Add a PictureBox and a Button. Also use the visual studio designer to add an EventHandler for if the operator clicks the button.
In the code of the User Control add the property to Set / Get an Image. The Image that you set will be displayed in the PictureBox; the Image that you get is the image that is displayed in the PictureBox.
Here comes the first chunk of C#:
public Image Image
{
get => this.pictureBox1.Image;
set => this.pictureBox1.Image = value;
}
I think in VB this will be very similar.
Now you want that users of your control (= software, not humanoids) get notified that the operator selects an image. Officially, you even want to hide how the operator selected this image: all you want to say: "Hey, the operator selected my image!"
Normally you would do this using an event:
The 2nd piece of C#:
public event EventHandler ImageSelected;
The event won't have any data. It will only say: "Hey, the operator selected my image!". The event event handler will have to find out which user control raised the event, and fetch the image.
Ok, raise the event:
private void OnImageSelected()
{
this.ImageSelected?.Invoke(this, EventArgs.Empty);
}
It will be a bit more difficult to translate this into VB. What it does, it checks if there is at least one person who wants my events. If so it raises the event. with two parameters: the sender (which is me), and no parameters. Later we will see how the receiver of the event works.
Finally: if the operator clicks the button, he indicates that he wants to select the image in the user control, and thus OnImageSelected needs to be called
private void ButtonSelect_Clicked(object sender, EventArgs e)
{
this.OnImageSelected();
}
We've finished creating the UserControl. After Compilation you will find it in visual studio designer toolbox.
So we'll go to the form that will have to display the eleven PictureBox-Button combinations. Let's call this form: ImageSelectionForm.
Using Visual Studio Designer, add 11 ImageSelectionControls. If you know already at design time which control will show which image, you can do this in the designer, similarly to how you set the text or the background of a button.
If you only know at run-time which control should show which image, consider to add a method, for instance if you want to load it from file:
void ShowImage(ImageSelectionControl imageSelectionControl, string imageFileName)
{
Image image = this.LoadImageFromFile(imageFileName);
this.imageSelectionControl.Image = image;
}
Or maybe you have a different method to decide which ImageSelectionControl should displaye which image. I guess you get the gist.
Almost done. ImageSelectionForm needs to be notified about the selected image, so just like you would do with a button, you need to subscribe to event ImageSelected.
In the constructor or ImageSelectionForm, subscribe to the events:
private ImageSelectionForm()
{
InitializeComponent();
// subscribe to events
this.controlBlueGeod.ImageSelected += this.ImageSelected;
this.controlAbstract.ImageSelected += this.ImageSelected;
...
}
Of course, you can put all ImageSelectionControls in one list and use foreach to subscribe (that is too much VB for me, no idea how to do this)
The event handler:
void ImageSelected(object sender, EventArgs e)
{
// we know that the sender is the one that holds the selected images:
ImageSelectionControl imageSelectionControl = (ImageSelectionControl)sender;
We will be closiing the form very soon, so we will have to remember this selected image somewhere in a property:
public Image SelectedImage {get; set;}
So continuing the event handler:
this.SelectedImage = imageSelectionControl.Image;
this.DialogResult = DialogResult.OK;
this.Close();
}
So the selected image is saved. The dialog result is set to indicate that an image was selected, not cancel clicked. And the form is closed.
Showing the form and process the selected image
The form that has to show ImageSelectionForm has a procedure:
private void ShowImageSelectionForm()
{
using (ImageSelectionForm dialog = new ImageSelectionForm())
{
// show the dialog and process the result:
DialogResult dlgResult = dialog.ShowDialog(this);
if (dlgResult == DialogResult.OK)
{
this.ProcessSelectedImage(dialog.SelectedImage);
}
else
{
// TODO: process cancel
}
}
}
There is one memory leak: if you created a lot of images, all images need to be Disposed after usage. Best is to by overriding Component.Dispos(bool)
ImageSelectionControl:
protected override Dispose (bool disposing)
{
if (disposing && this.Image != null)
{
this.Image.Dispose();
}
}
ImageSelectionForm:
protected override Dispose (bool disposing)
{
if (disposing)
{
this.controlBlueGeod.Dispose();
this.controlAbstract.Dispose();
...
}
}
This is quite a nuisance for 11 controls. Therefore quite often a Form or a UserControl has a System.ComponentModel.IContainer components. If you add your ImageSelectionControls to this container, then the ImageSelectionControls will be automatically disposed when ImageSelectionForm is disposed.
You have two Controls that work together to allow users to preview and select one or more Images in the UI. When you have this kind of scenario, build a UserControl that contains all the required UI elements and the logic used perform this action.
To build this UserControl:
Open the Project menu and select Add User Control...
Right away, set (in this order):
AutoScaleMode = Dpi
Font = Segoe UI, 9pt
BackColor = Color.Transparent
Set a Size that you think is the minimum required to show an Image and a Button
When you have found the right size, set also the MinimumSize to this value
Add a Button to the UC, configure it as needed, set its Width as the Width of the UC minus 4 pixels. Position the Button at the bottom of UC, 2 pixels to the left and set Anchor = Bottom, Left, Right
Add a PictureBox, same Width and Left location as the Button. Set the Height as required, then Anchor = Top, Bottom, Left, Right
Design completed. Build the Project.
Now, your UserControl needs to provide means to set an Image to the PictureBox and the Text of the Button.
You can add two Public Properties of Type Image and String that, when a value is assigned, set the PicureBox.Image property and the Button.Text property.
Also, when the Button is clicked, the UserControl needs to notify that the User has performed a selection, so whatever other code needs to do something with this information, will be able to act on it.
In an event-driven UI, you make your UC raise a public event that interested parties can subscribed to, to receive a notification that something happened.
You just need to define a Field of Type Event and give it a name. In this case, e.g.:
Public Event ImageSelection As EventHandler
When the Button is clicked, you raise the event:
RaiseEvent ImageSelection(Me, EventArgs.Empty)
The sample UserControl is named ImagePicker, the PictureBox is named picImage and the Button is btnSelectImage
Public Class ImagePicker
Public Event ImageSelection As EventHandler
Private m_Picture As Image = Nothing
Private m_Title As String
Public Property Picture As Image
Get
Return m_Picture
End Get
Set
m_Picture = Value
picImage.Image = m_Picture
End Set
End Property
Public Property Title As String
Get
Return m_Title
End Get
Set
m_Title = Value
btnSelectImage.Text = m_Title
End Set
End Property
Private Sub btnSelectImage_Click(sender As Object, e As EventArgs) Handles btnSelectImage.Click
RaiseEvent ImageSelection(Me, EventArgs.Empty)
End Sub
End Sub
In your Form (for example), you can the subscribe to this event to receive a notification that the User has selected the Image of one of your UserControls.
When the event is raised, the sender object is the UserControl that raised the event. You can then cast sender to your UserControl Type and read the Public Property that references the Image you have assigned to it.
In a Form, you can add some of your UserControls to a FlowLayoutPanel (in the example, named flpPreviews), which will take care of the layout of these Controls.
While adding the UserControls, you set their Public Properties and also subscribe to the Public event.
When the event is raised, cast sender to ImagePicker and read its public Picture property to get the reference of the Image selected:
Public Class SomeForm
Public Sub New()
InitializeComponent()
' Add three ImagePicker UserControls to a FlowLayoutPanel
For i As Integer = 0 To 2
Dim picker = New ImagePicker()
picker.Picture = [Some Image]
picker.Title = "Some Title"
' Subscribe to the event
AddHandler picker.ImageSelection, AddressOf ImageSelectionChanged
flpPreviews.Controls.Add(picker)
Next
End Sub
Private Property SelectedImage As Image
Private Sub ImageSelectionChanged(sender As Object, e As EventArgs)
SelectedImage = DirectCast(sender, ImagePicker).Picture
' Show the selected Image in a PictureBox, child of the Form
picSelected.Image = SelectedImage
End Sub
End Class
I have two labels in my form which are placed side-by-side to act as one label. When I hover over the labels, I have a function that fades the labels to different colours, which works well. I am trying to apply the MouseHover and MouseLeave event to both labels, so that when I hover over Label1 and move to Label2 (and vice versa), the function doesn't then fade the colour back to the original colour. Currently, moving between the two labels activates MouseLeave followed by MouseHover again in the new label.
I have tried to add both labels to the event trigger, but this hasn't worked. I have also tried placing both labels in a Panel, but that then doesn't trigger the event.
Private Sub fadeHeaderIn(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Label1.MouseHover, Label2.MouseHover
Call fadeLabel("In")
End Sub
Private Sub fadeHeaderOut(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Label1.MouseLeave, Label2.MouseLeave
Call fadeLabel("Out")
End Sub
If the functionality to change the colour of part of the label existed, I'd not need the two labels, so if there is a better way of doing this altogether, I'm happy to do so. Thanks!
I have also tried placing both labels in a Panel, but that then
doesn't trigger the event.
That should work. The panel would act as the boundaries for both labels. You'll get a MouseLeave, though, when you move from the panel to the labels contained within. To prevent a false trigger, simply check if the mouse is still within the bounds of the panel. You can prevent multiple fade ins when moving from label to label by tracking the faded state with a boolean. It'd look something like this:
Public Faded As Boolean = False
Private Sub fadeHeaderIn(sender As Object, e As EventArgs) Handles Label1.MouseHover, Label2.MouseHover
If Not Faded Then
Faded = True
fadeLabel("In")
End If
End Sub
Private Sub fadeHeaderOut(sender As Object, e As EventArgs) Handles Panel1.MouseLeave
If Not Panel1.ClientRectangle.Contains(Panel1.PointToClient(Cursor.Position)) Then
If Faded Then
Faded = False
fadeLabel("Out")
End If
End If
End Sub
Here's a sample of what has been described in comments.
The Text of a control (here, derived from a standard Label) is split in two sections of the same measure. Each section can have a different color.
The active and inactive colors are custom public properties, they can be set in the designer.
Each section is tracked, meaning that the control is aware of what side the Mouse Pointer is currently hovering.
The size of the text is measured using the TextRenderer.MeasureText method. This size is used to calculate the rectangles that include the sections of text.
The Rectangle.Contains([Point]) method is then used to determine which section of the text the mouse pointer in hovering. [Point] is calculated using the MousePosition property, translated to client coordinates using the Control.PointToClient() method.
When the mouse pointer is moved from one section of text to the other (here, just two sections, more could be defined adding more rectangles), the control is Invalidated, causing a call to the OnPaint method of the control.
If the mouse pointer is not hovering a section of text, base.OnPaint(e) is called (also causing the Paint event to raise), which draws the default text with the default color.
In the OnPaint method, the Graphics region is clipped using the Rectangles that define the text sections. A subsequent call to TextRenderer.DrawText, setting the TextFormatFlags.PreserveGraphicsClipping flag, clips the text in the defined region, so just the section of text that fits in the clipping region is painted.
The Graphics.ExcludeClip() method is used here to define these clipping regions.
The TextFormatFlags.ExternalLeading and TextFormatFlags.TextBoxControl are also used to replicate the default text rendering, so the custom text is rendered in the same relative position.
This is how it behaves:
Custom Control Class to test the functionality:
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
<DesignerCategory("Code")>
Public Class LabelSplitText
Inherits Label
Private m_Text As String = String.Empty
Private m_Sections As RectangleF() = Nothing
Private m_PaintText As Boolean = False
ReadOnly flags As TextFormatFlags = TextFormatFlags.ExternalLeading Or
TextFormatFlags.PreserveGraphicsClipping Or
TextFormatFlags.TextBoxControl
Public Sub New()
InitializeComponent()
End Sub
Private Sub InitializeComponent()
ResizeRedraw = True
End Sub
Public ReadOnly Property ActiveRectangle As RectangleF
Public ReadOnly Property ActiveSide As String = String.Empty
Public Property ActiveColor As Color = Color.White
Public Property InactiveColor As Color = Color.DimGray
Protected Overrides Sub OnLayout(e As LayoutEventArgs)
MyBase.OnLayout(e)
Me.AutoSize = False
m_Text = Me.Text
End Sub
Protected Overrides Sub OnMouseEnter(e As EventArgs)
m_Text = Me.Text
Text = String.Empty
m_PaintText = True
MyBase.OnMouseEnter(e)
Invalidate()
End Sub
Protected Overrides Sub OnMouseLeave(e As EventArgs)
m_PaintText = False
Me.Text = m_Text
MyBase.OnMouseLeave(e)
End Sub
Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
MyBase.OnMouseMove(e)
Invalidate()
If m_Sections Is Nothing Then Return
Me._ActiveRectangle = If(m_Sections(0).Contains(e.Location), m_Sections(0), m_Sections(1))
End Sub
Protected Overrides Sub OnMouseClick(e As MouseEventArgs)
Me._ActiveSide = If(m_Sections(0).Contains(e.Location), "left", "right")
MyBase.OnMouseClick(e)
End Sub
Protected Overrides Sub OnPaint(e As PaintEventArgs)
If Not m_PaintText Then
MyBase.OnPaint(e)
Return
End If
Dim textSize As SizeF = TextRenderer.MeasureText(e.Graphics, m_Text, Me.Font, Me.ClientSize, flags)
m_Sections = GetTextAreaSections(textSize)
e.Graphics.ExcludeClip(Rectangle.Round(m_Sections(1)))
TextRenderer.DrawText(e.Graphics, m_Text, Me.Font, Point.Empty, GetSectionColor(0), flags)
e.Graphics.ResetClip()
e.Graphics.ExcludeClip(Rectangle.Round(m_Sections(0)))
TextRenderer.DrawText(e.Graphics, m_Text, Me.Font, Point.Empty, GetSectionColor(1), flags)
End Sub
Private Function GetSectionColor(section As Integer) As Color
Return If(m_Sections(section).Contains(PointToClient(MousePosition)),
Me.ActiveColor, Me.InactiveColor)
End Function
Private Function GetTextAreaSections(textSize As SizeF) As RectangleF()
If textSize.Width > Me.ClientSize.Width Then textSize.Width = Me.ClientSize.Width
Dim rectLeft = New RectangleF(PointF.Empty,
New SizeF(textSize.Width / 2.0F, Me.ClientSize.Height))
Dim rectRight = New RectangleF(New PointF(textSize.Width / 2.0F, 0),
New SizeF(textSize.Width / 2.0F, Me.ClientSize.Height))
Return {rectLeft, rectRight}
End Function
End Class
I've created a usercontrol.
I've placed a Button into it.
Now when I click the button, I would like to raise the default Click event.
For that, I added the following code:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
RaiseEvent Click(sender, e)
End Sub
What am I doing wrong here?
This is the entire code of the usercontrol:
Imports System.ComponentModel
Public Class ucColorButton
<Browsable(True)>
Public Overrides Property BackColor() As Color
Get
Return Me.Button1.BackColor
End Get
Set(value As Color)
Me.Button1.BackColor = value
End Set
End Property
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
RaiseEvent Click(sender, e)
End Sub
End Class
The compiler tells me:
There's no RaiseEvent definition for the event "Click"
You don't use RaiseEvent to raise an inherited event. This is why all events should have an associated method. To raise the Click event you call the OnClick method and that is the only place that RaiseEvent is used. If you want to change the behaviour on a Click event then you override that method, otherwise you just accept the default behaviour from the base class. To see how events are properly implemented - and are implemented in the base classes you're inheriting - check this out.
Also, while it technically doesn't matter in this case, you shouldn't really be passing the e parameter from your internal event handler to your external event. You should be creating your own EventArgs object as required by your event.
Finally, if you were to be able to use RaiseEvent, it would be wrong to pass on the sender parameter too. The sender is ALWAYS supposed to be the object that raised the event. In your case, that is the user control, NOT the internal Button. Fortunately, calling OnClick will fix that. If you needed to pass on information about which child control was clicked then you should be defining your own event and passing that information via the e parameter.
I'm making a custom ComboBox, and I'm struggling to make it's dropdown (a Panel) close when the user clicks outside. As everyone probably knows, using LostFocus event is not enough because when the user clicks on eg.: a scroolbar or the form itself, the control doesn't lose focus. I tried using IMessageFilter, but I don't think I understood how it works. I also tried using the Capture property, forcing the dropdown to capture the mouse, but that also forces the mouse click to happen on the dropdown (Panel) itself, meaning that if the user had clicked on an item on the dropdown, the click won't work.
Let me clarify:
I'm making a custom control, a UserControl, which is compiled to a .DLL, and can be dragged'n dropped from the Toolbox onto forms, just like any other Winforms control.
The control is a Combobox.
I want to make the Combobox's dropdown (which is a panel, created at runtime) close when the user clicks outside of it, just like a normal ComboBox.
And what is the problem? Detecting clicks outside of the panel and then closing it.
-> Using LostFocus event: Not enough. If the user clicks on a blank space on the form, LostFocus is not fired. I know I can place a LostFocus event on my form and set the ActiveContrl to Nothing, but that would require that users (devs) of my custom ComboBox place the same code on every form they use the control.
-> Using Capture property: Has given the best results so far. There are still some problems though. When Capture is set to True, the mouse is indeed captured by the control which has that property set to True. Therefore, no mouse activity is sensed by any other control on the app, but only the control which has the mouse captured. It'll only release the mouse when the user performs a click. This leads to some problems:
If the user clicks on an item (a Button) on the dropdown, and the dropdown (a Panel) has its Capture property set to true, the click will be "ignored", as it'll be handled by the dropdown, and not by the Buttonon which the user actually wanted to click.
If the user clicks on the dropdown's scroolbar, the click will also be "ignored", just like explained above.
When user moves the mouse above the Buttons inside the dropdown (the ComboBox's items), they are not highlighted, because no MouseEnter is fired for them, as the mouse is Captured by the dropdown.
There is a way to solve the first issue: You can find on which button is the mouse pointing using Control.MousePosition, and then force a click on it.
I haven't been able to find a way to solve the 2nd and 3rd issue though. One possible solution I thought of was setting the dropdown's Capture property to False when the mouse enters on it. So, this is how it would work:
User clicks on the Combobox. Dropdown opens, captures the mouse;
User then moves the mouse inside Combobox's dropdown. Capture is set to false, thus making it possible for the user to click on any button inside the dropdown or on the dropdown's scroolbar, and so forth. Buttons inside the dropdown are also properly highlighted as user moves the mouse above them;
User moves the mouse outside Combobox's dropdown, Capture is again set to true, and now any click the user perform outside of it will be "ignored", and the dropdown will be closed. Just like a normal Combobox.
But when I tried to do this, another issue arrived: when a control has its Capture property set to True, it'll constantly fire MouseEnter events. MouseEnter event doesn't use the real mouse pointer location. Even if the mouse pointer is actually outside a Control, that Control will think the mouse is actually inside of it if its Capture property is set to True.
Edit2: here is the code for handling some different types of events (should work for all cases now).
Public Class Form1
Private Shared mouseNotify() As Int32 = {&H201, &H204, &H207} ' WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN
Private Shared scrollNotify() As Int32 = {&H114, &H115} ' WM_HSCROLL, WM_VSCROLL
Private Shared scrollCommands() As Int32 = {0, 1, 2, 3, 4, 5} ' SB_LINEUP, SB_LINEDOWN, SB_PAGEUP, SB_PAGEDOWN, SB_THUMBTRACK, SB_THUMBPOSITION
Private Sub baseLoad(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
AutoScroll = True
Controls.Add(myPanel)
Controls.Add(myTextBox4)
myPanel.myTextBox1.Focus()
End Sub
Private myTextBox4 As New customTextBox(300)
Private myPanel As New customPanel
Protected Overrides Sub OnScroll(ByVal se As ScrollEventArgs)
MyBase.OnScroll(se)
ActiveControl = Nothing
End Sub
Protected Overrides Sub OnMouseWheel(ByVal e As MouseEventArgs)
MyBase.OnMouseWheel(e)
ActiveControl = Nothing
End Sub
Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
MyBase.OnResize(e)
ActiveControl = Nothing
End Sub
Protected Overrides Sub OnMove(ByVal e As System.EventArgs)
MyBase.OnMove(e)
ActiveControl = Nothing
End Sub
Friend Shared Function isOverControl(ByRef theControl As Control) As Boolean
Return theControl.ClientRectangle.Contains(theControl.PointToClient(Cursor.Position))
End Function
Protected Overrides Sub WndProc(ByRef m As Message)
If mouseNotify.Contains(CInt(m.Msg)) Then
If Not isOverControl(myPanel) Then
ActiveControl = Nothing
Else
myPanel.myTextBox1.Focus()
End If
End If
MyBase.WndProc(m)
End Sub
Friend Class customPanel : Inherits Panel
Friend myTextBox1 As New customTextBox(20)
Private myTextBox2 As New customTextBox(60)
Private myTextBox3 As New customTextBox(200)
Friend Sub New()
AutoScroll = True
Location = New Point(0, 100)
Size = New Size(200, 100)
Controls.Add(myTextBox1)
Controls.Add(myTextBox2)
Controls.Add(myTextBox3)
End Sub
Protected Overrides Sub OnLeave(ByVal e As EventArgs)
MyBase.OnLeave(e)
myTextBox1.Text = "false"
myTextBox2.Text = "false"
BackColor = Color.Green
End Sub
Protected Overrides Sub OnEnter(ByVal e As EventArgs)
myTextBox1.Text = "true"
myTextBox2.Text = "true"
BackColor = Color.Gold
MyBase.OnEnter(e)
End Sub
Protected Overrides Sub WndProc(ByRef m As Message)
If mouseNotify.Contains(CInt(m.Msg)) Then
If isOverControl(Me) Then Form1.WndProc(m)
End If
MyBase.WndProc(m)
End Sub
End Class
Friend Class customTextBox : Inherits TextBox
Friend Sub New(ByVal y As Integer)
Location = New Point(10, y)
Size = New Size(100, 30)
End Sub
Protected Overrides Sub OnLeave(ByVal e As EventArgs)
MyBase.OnLeave(e)
BackColor = Color.Blue
End Sub
Protected Overrides Sub OnEnter(ByVal e As EventArgs)
BackColor = Color.Red
MyBase.OnEnter(e)
End Sub
End Class
End Class
If it doesn't work in all cases, you may have to attach events to all the controls on your form that can't/won't receive focus on mouse events.
also, this works slightly differently depending on the type of control. richtextbox use OnHScroll and OnVscroll for example, instead of OnScroll. you can also get the thumb position with CInt(m.WParam.ToInt32 >> 16), only valid for SB_THUMBTRACK, SB_THUMBPOSITION.
also, here are some interesting techniques: Handling a click event anywhere inside a panel in C#
this was actually taken from the MSDN page for WndProc: http://msdn.microsoft.com/en-us/library/system.windows.forms.control.wndproc%28v=vs.110%29.aspx
and the page for NativeWindow Class: http://msdn.microsoft.com/en-us/library/system.windows.forms.nativewindow.aspx
I hope I can make it clear what my problem is. I have made my own Chart control
Public Class MyChart
Inherits Chart
[...]
End Class
In this class I handle the MouseDown event of the chart and do stuff like zooming, dragging markers and so on.
In the project I am using the chart I also handle the MouseDown event to do some more specific stuff. Now there is this problem:
In the MouseDown event handler in the MyChart class I use the middle mouse button to drag the chart.
However in the project MouseDown event handler I check if the user hit a specific object and want to let him drag this object also with the middle mouse button. The problem is, that the handler in MyChart is executed first so I can't check if the user hit the object (and therefore initiate the dragging of this object).
What I need is that the event handler in the project is executed first and after this the one in MyChart.
To hopefully make it more clear:
(Dummy Code)
Public Class SomeProject
WithEvents Chart as MyChart
Public Sub Chart_MouseDown(sender as object, e as MouseEventArgs) Handles Chart.MouseDown
'This is executed second, but should be executed first.
End Sub
End Class
Public Class MyChart
Inherits Chart
Public Sub MyChart_MouseDown(sender as object, e as MouseEventArgs) Handles Me.MouseDown
'This is executed first, but should be executed second.
End Sub
End Class
Is there any way I can do this?
Try creating your own event:
Public Class MyChart
Inherits Chart
Public Event ChartMouseDown(sender As Object, e As MouseEventArgs)
Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
RaiseEvent ChartMouseDown(Me, e)
MyBase.OnMouseDown(e)
// your second code here...
End Sub
End Class
Then your project code would look like this:
Public Class SomeProject
WithEvents Chart as MyChart
Public Sub Chart_ChartMouseDown(sender as object, e as MouseEventArgs) _
Handles Chart.ChartMouseDown
// your first code here...
End Sub
End Class