How do I get my Windows Forms drawn user control to resize larger without clipping? - vb.net

I am in the process of creating my own user control in a Windows Forms application using VB.NET, and having it change in size along with the form containing it. It looks okay so long as the window size stays constant and the control stays within its original bounds. However, if I resize the window so as to make it larger, the control's contents will resize with it but end up getting clipped at the original size. I am unsure where this is coming from and have not found any way so far to fix it.
Below is some quick sample code for a user control which can reproduce the problem I'm having:
TheCircle.vb:
Public Class TheCircle
Private _graphics As Graphics
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_graphics = CreateGraphics()
SetStyle(ControlStyles.ResizeRedraw, True)
BackColor = Color.Red
End Sub
Private Sub TheCircle_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
_graphics.FillRectangle(Brushes.Blue, ClientRectangle)
_graphics.FillEllipse(Brushes.LimeGreen, ClientRectangle)
End Sub
End Class
I then place this user control after rebuilding the project in my main form and either dock it or anchor it (same result, but the latter helps better show where the clipping issues are). And below is a screenshot of my result when I attempt to resize the control beyond its "default" size:
The green ellipse and the blue "background" rectangle should occupy the entire control area, but they aren't (it's clipped and you see the red BackColor instead). It looks like it behaves as intended though when the control is at the original size or smaller. How can I fix this? I'm pretty new to GDI+, so I'm sure it must be right under my nose...

This is unnecessary, and a generally bad idea: _graphics = CreateGraphics()
It's bad because it's volatile. You get a one-time graphics object, draw something with it, and then the next refresh cycle, it's lost unless you keep doing it.
The proper approach is to use the Paint event, as it supplies you with the Graphics object in its PaintEventArgs and it is called every time a re-paint is required. You can ask for a re-paint at any point in your code by calling theCircle.Invalidate() (or Refresh() for a more immediate redraw).
Public Class TheCircle
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
SetStyle(ControlStyles.ResizeRedraw, True)
BackColor = Color.Red
End Sub
Private Sub TheCircle_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
e.Graphics.FillRectangle(Brushes.Blue, ClientRectangle)
e.Graphics.FillEllipse(Brushes.LimeGreen, ClientRectangle)
End Sub
End Class

Related

Most efficient way to programmatically update and configure forms and controls

I am looking for a way to prevent user form controls from appearing one by one when I'm programmatically adding them and for ways to enhance application performance and visual appeal.
Say, I have a Panel_Top in which I programmatically add comboboxes. What is happening is that they appear one by one as they are created and I am looking for a way to suspend the refreshing of the panel and or user form to make all of those programmatically added comboboxes to appear at the same time and faster than it happens right now.
I've tried suspendlayout which doesn't do anything for me or maybe I'm doing it wrong.
MyForm.PanelTop.SuspendLayout = true
And also I've tried to set the Panel_Top to invisible like:
MyForm.Top_Panel.visible = false
Which kind of sorta looks and performs better, or it might be a placebo.
What is the correct approach to this problem?
PS: I do have form set to doublebuffer = true, if that matters
What I tend to do is create a loading modal to appear on top of the form rendering the controls that need to be created/made visible, this can optionally have a progress bar that gets incremented as the control is created/shown. With the loading modal running, the container that needs to add the controls starts with SuspendLayout, adds the controls, and then finished with ResumeLayout.
This makes it so that controls are added/shown while giving the user a visual indicator that something is going on behind the scenes.
Here is a phenomenal example of a loading modal: https://www.vbforums.com/showthread.php?869567-Modal-Wait-Dialogue-with-BackgroundWorker and here is an example of using it:
Private ReadOnly _controlsToAdd As New List(Of Control)()
Private Sub MyForm_Show(sender As Object, e As EventArgs) Handles MyBase.Shown
Using waitModal = New BackgroundWorkerForm(AddressOf backgroundWorker_DoWork,
AddressOf backgroundWorker_ProgressChanged,
AddressOf backgroundWorker_RunWorkerCompleted)
waitModal.ShowDialog()
End Using
End Sub
Private Sub backgroundWorker_DoWork(sender As Object, e As DoWorkEventArgs)
Dim worker = DirectCast(sender, BackgroundWorker)
For index = 1 To 100
_controlsToAdd.Add(New ComboBox() With {.Name = $"ComboBox{index}"})
worker.ReportProgress(index)
Threading.Thread.Sleep(100) ' Zzz to simulate a long running process
Next
End Sub
Private Sub backgroundWorker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs)
Dim percentageCompleted = e.ProgressPercentage / 100
' do something with the percentageCompleted value
End Sub
Private Sub backgroundWorker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
PanelTop.SuspendLayout()
PanelTop.Controls.AddRange(_controlsToAdd.ToArray())
PanelTop.ResumeLayout()
End Sub
SuspendLayout() is the correct way to handle this with WinForms.
But first of all, this is a function you call, and not a flag you set.
Secondly, don't forget to call ResumeLayout() at the end of the changes.
Finally, you need to ensure you only call them once when you start to change around the controls in the panel and then again at the very end. If you use them with every control you won't get any benefit.
So the pattern might look something like this:
Public Sub SomeMethod()
PanelTop.SuspendLayout() ' Prevent the panel from updating until you've finished
' Make a bunch of changes
PanelTop.Controls.Clear()
For Each ...
PanelTop.Controls.Add( ... )
Next
PanelTop.ResumeLayout() ' Allow the panel to show all the changes in the same WM_PAINT event
End Sub
You also need to ensure you don't have anything in there like DoEvents()/Invalidate() that might invoke the windows message loop and cause the form to redraw itself.

Where to declare a public object which refer to a VB object?

First time that I use Visual Studio and VB.net.
Could someone explain to me where to declare a public object which refers to a VB object ?
This code works fine :
Public Class Form1
Private ThePen As New System.Drawing.Pen(Color.Red)
Private Sub Line(A As Point, y As Point)
Dim NewGraphic As Graphics = PictureBox1.CreateGraphics()
NewGraphic.DrawLine(ThePen, A, B)
NewGraphic.Dispose()
End Sub
End Class
But I would like to declare only one time in public
Dim NewGraphic As Graphics = PictureBox1.CreateGraphics()
I tried to declare it at the beginning, but it seem that my object PictureBox1 is not yet loaded (so, can't access PictureBox1.CreateGraphics())
So I tried in
Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
But I can't declare public variable inside :(
You should pretty much NEVER call CreateGraphics. Draw on a control in its Paint event handler or, if appropriate, create a custom control and override the OnPaint method. Store the data that represents the drawing in one or more fields and, whenever you want to change the drawing, set those fields and call Invalidate on the control.
Private lineStart As Point
Private lineEnd As Point
Private Sub DrawLine(start As Point, [end] as Point)
lineStart = start
lineEnd = [end]
PictureBox1.Invalidate()
End Sub
Private Sub PictureBox1_Paint(sender As Object, e As EventArgs) Handles PictureBox1.Paint
e.Graphics.DrawLine(Pens.Red, lineStart, lineEnd)
End Sub
Generally speaking, it is preferable to specify the area to invalidate rather than not specifying an argument and invalidating the entire control. It is actually painting the pixels to the screen that is the slow part so it is preferable to keep that to a minimum. I'll leave that part to you but you may like to check this out for more info. Note that, if you're moving a line, you'd need to invalidate the area that contained the old line and the area that will contain the new line. You can call Invalidate multiple times with different areas in such cases, or you can combine the areas into one Region and call it once.
So different view but interesting !
I'm unable to test it :(
There's a problem with e. object in
e.Graphics.DrawLine(Pens.Red, lineStart, lineEnd)
BC30456 Visual Basic 'Graphics' is not a member of 'EventArgs'.

Setting a windows form's screen position based on another form's current position

I am creating an application with multiple windows forms. The main form is movable, and I want a confirmation window to flash based on where the main form is located.
For example, the main form opens, user drags it 200 points to the left. How do I make sure the confirmation window, upon button-press, opens up exactly to the left of that window?
The built-in properties (center screen, center parent, etc.) don't provide this functionality.
I'm aware of these functions:
Form1.Left += 200
and
Dim frmAccounts as new Form()
Set FrmAccounts.DesktopLocation = new Point(100,100)
but neither of these take into consideration user dragging.
Any ideas?
Thanks for your help.
To keep the buddy glued to the main form, you have to use the main form's LocationChanged event to know when to move it. And you have to position it before it is displayed, that's a bit tricky due the form possibly getting rescaled on a machine with a different DPI setting. Best time to do it is when the buddy's Load event fires, it is rescaled by then. Some sample code:
Public Class Form1
Dim buddy As Form
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If buddy Is Nothing Then
buddy = New Form2
AddHandler buddy.Load, AddressOf MoveBuddy
AddHandler Me.LocationChanged, AddressOf MoveBuddy
AddHandler buddy.FormClosed, Sub() buddy = Nothing
buddy.Show(Me)
End If
End Sub
Private Sub MoveBuddy(sender As Object, e As EventArgs)
buddy.Bounds = New Rectangle(Me.Left - buddy.Width, Me.Top, buddy.Width, buddy.Height)
End Sub
End Class

Darken a .Net Form

I have a 1080p touchscreen application. When a modal pops up, i want to emphasize that by darkening the main form.
Right now i use a second form, the size of the main form, that is black and has 50% opacity. Whenever a modal needs to appear, i open the opaque form, and then open the desired modal.
I feel this is a bit devious for my purpose. Its also not asshole-proof that when the user alt tabs, the forms will glitch out of sequence.
Is there a better way to achieve the darkening effect. Perhaps by darkening the main form from within itself?
Solved it myself by doing the following:
Place a hidden picturebox with dock:fill on the main form,
Take a screenshot of the current screen and darken it
assign the image to the picturebox and make it visible
open the modal in a new win
when the modal is dismissed
hide the picturebox
It really stupid that VB.net doesn't have this function built into it. Here's what you do to get around it:
Make a new form and call it Shade. I'm going to assume your main form is called frmMain. For the sake of clarity, lets assume the form you're launching is called dlgX.
Add the following lines in the Load event of dlgX (that's the sub with dlgX.Load or Me.Load or MyBase.Load):
Shade.Opacity = 0.001
Shade.Show()
Shade.Location = frmMain.Location ' Form location will only update if the form is visible.
Shade.Hide()
Shade.FormBorderStyle = Windows.Forms.FormBorderStyle.None 'This gets rid of the windows Titlebar and window border.
Shade.Size = frmMain.Size
Shade.BackColor = Color.Black
Shade.Opacity = 0.5
Shade.Show() ' Form size will only update the next time you show it.
Shade.TopMost = True ' Puts Shade over main form
Me.TopMost = True ' Puts current form over shade
Under all events that dismiss the form dlgX (OK.click, Cancel.click, etc), add the following lines:
Shade.Close
Or you can even make your own sub that handles all events where the form is closed:
Private Sub DispelShades(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.FormClosed
Shade.Close()
End Sub
This is way simpler than the PictureBox scenario and you don't have to mess with layering issues and having to ensure that the PictureBox renders on top of everything (for example, tabs really do not like having things rendered above them and they will not let you render a picture box above them). Rendering a black semi transparent form above your main form gets around all these headaches.
If you have multiple forms to shade, just make a Shad1, Shade2, Shade3 etc.
This is pretty obvious but it's worth stating: if you're shading the main form, you'll also want to make it unclickable by opening dlgX via dlgX.ShowDialog and not dlgX.Show
Here is some code, very similar to the method in Thomas's answer. Note to use the Darkness property in a Try...Finally block, to make sure you never leave the form in the dark state.
Public Class Form1
Private _PB As PictureBox
Public WriteOnly Property Darkness
Set(value)
If value Then
Dim Bmp = New Bitmap(Bounds.Size.Width, Bounds.Size.Height)
Me.DrawToBitmap(Bmp, New Rectangle(Point.Empty, Bounds.Size))
Using g = Graphics.FromImage(Bmp)
Dim Brush As New SolidBrush(Color.FromArgb(125, Color.Black))
g.FillRectangle(Brush, New Rectangle(Point.Empty, Bmp.Size))
End Using
_PB = New PictureBox
Me.Controls.Add(_PB)
_PB.Size = Bounds.Size
_PB.Location = Bounds.Location - PointToScreen(Point.Empty)
_PB.Image = Bmp
_PB.BringToFront()
Else
If _PB IsNot Nothing Then
Me.Controls.Remove(_PB)
_PB.Dispose()
End If
End If
End Set
End Property
Private Sub btnDialog_Click(sender As Object, e As EventArgs) Handles btnDialog.Click
Try
Darkness = True
MsgBox("Modal dialog")
Finally
Darkness = False
End Try
End Sub
End Class

How can I set a form to have a transparent background

I am struggling to get my form to have a transparent background in vb.net
Currently in the form New I set
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, true)
But still the form shows up as having the default grey background
Can anyone help??
EDIT: I need the controls on the form to be visible so I don't think setting the opacity to 0 will work
EDIT: I tried the transparency key solution but it doesn't work. I have a circular image with a black background. OnPaint I set the transparency key to the img pixel at 0,0, this then leaves me with circular image (which I want ) It hides the black background but I am still left with the default grey rectangle of the form.
below is the code I have -
Public Sub New()
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True)
Me.BackColor = Color.Transparent
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.Timer1.Start()
End Sub
Private Sub frmWoll_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Dim img As Bitmap = CType(Me.BackgroundImage, Bitmap)
img.MakeTransparent(img.GetPixel(2, 2))
Me.TransparencyKey = img.GetPixel(2, 2)
End Sub
Use TransparencyKey for transparent form.
eg.
TransparencyKey = Color.Red
Button1.BackColor = Color.Red
Now run the form you will find that the button1 has a hole in it.
So using this method you can create a mask image in paint for which part has to be transparent and apply that image to form and voila the form is now transparent.
Edit:
Sorry for late reply.
Following is your code modified to suit your requirement
Public Sub New()
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True)
Me.BackColor = Color.Transparent
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Dim img As Bitmap = CType(Me.BackgroundImage, Bitmap)
'img.MakeTransparent(img.GetPixel(2, 2))
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.None
Me.TransparencyKey = img.GetPixel(2, 2)
End Sub
Set Form's TransparencyKey color property same as form's Background color property
There are a few methods you could use.
Use the forms TransparencyKey
Override OnPaintBackground (WM_ERASEBKGND)
Override WndProc and handle the paint messages (WM_NCPAINT, WM_PAINT, etc)
I recommend overriding the window procedure to get optimal results.
Me.Opacity = 0
Be warned that:
This is for the entire form, rather than just the background. There are work-arounds to make certain parts more opague.
It's only psuedo-transparency where it takes a snapshot of what's behind it. It's smart enough to know when you move the form, but not when you move other transparent objects on top of the form.