Scaling form controls according to screen resolution - vb.net

I am trying to write program in vb 2010 that is independent of screen resolution.
I am designing the program in 1920*1080 and when I change the resolution to e.g. 800*600 everything blows up and the program won't fit the screen. How can I fix this?
I have tried three different approaches:
loop through all controls and scale their position and dimensions
Friend Sub ResizeControl(ByRef ctl As Control)
'---------------------------- GET SCALES -------------------------
Dim DesignScreenWidth As Integer = 1920
Dim DesignScreenHeight As Integer = 1080
Dim CurrentScreenWidth As Integer = Screen.PrimaryScreen.Bounds.Width
Dim CurrentScreenHeight As Integer = Screen.PrimaryScreen.Bounds.Height
'Ratios
Dim ratioX As Double = CurrentScreenWidth / DesignScreenWidth ' e.g. 800/1920
Dim ratioY As Double = CurrentScreenHeight / DesignScreenHeight
With ctl
Dim height As Integer = Math.Min(.Height, CurrentScreenHeight)
Dim width As Integer = Math.Min(.Width, CurrentScreenWidth)
'Position
If (.GetType.GetProperty("Top").CanRead) Then .Top = CInt(.Top * ratioY)
If (.GetType.GetProperty("Left").CanRead) Then .Left = CInt(.Left * ratioX)
'Size
If (.GetType.GetProperty("Width").CanRead) Then .Width = CInt(width * ratioX)
If (.GetType.GetProperty("Height").CanRead) Then .Height = CInt(height * ratioY)
End With
'---------------------- RESIZE SUB CONTROLS -------------------------------
For Each subCtl As Control In ctl.Controls
ResizeControl(subCtl)
Next subCtl
End Sub
Anchor each control to the main Form and only resize the main form
tried to AutoScale Mode
Dim factorX As Double = ratioX * 96.0F
Dim factorY As Double = ratioY * 96.0F
Dim newSize As SizeF = New SizeF(factorX, factorY)
AutoScaleDimensions = newSize
AutoScaleMode = AutoScaleMode.Dpi
Scale(newSize)
Font = New Font(Font.FontFamily, Font.Size * factorX)
None of these methods has worked for me. What am I doing wrong?
One thing I figured out was that my main form is larger than 800*600 pixels so when I run the designer in 800*600 resolution VS cut down the with to 812px so my calculations of with and thus scaling ratio becomes wrong. This error goes applies for all three methods.
Please advise on the best method and if I am doing something wrong.

As an expansion to my first comment, take a look at some best practices from the UX (user experience) world. There is a lot of science and and deliberation put into UIs that work.
There's a great SE site - ux.stackexchange.com that you may find can answer your questsions better than SO.
Here's some questions you may find helpful:
https://ux.stackexchange.com/questions/3414/desktop-software-design-patterns (see MS & Apple have their own guidelines incl. things like button widths)
https://ux.stackexchange.com/questions/11361/responsive-web-design-technique-in-desktop-application
Responsive web design seems to parallel what you're doing. The idea behind it is to have your website be able to handle any client device - which is becoming very important because of the mobile explosion.
I suggest doing some studying in these areas to come up with a good solution.

Related

System.Drawing.Pen - lines disappear when Labels are placed on Form

I want to draw TextBoxes/Labels on my form in code and connect them with lines - based on data that I have stored in a datatable ("treedata"). If I use the following code everything works fine:
For i = 0 To treedata.Rows.Count - 1
Dim tb As New TextBox
hor = treedata.Rows(i)(11)
vern = ver + 120 * treedata.Rows(i)(4)
tb.Text = "sometext"
tb.Location = New Point(hor, vern)
Form8.Controls.Add(tb)
posofmodif = treedata.Rows(i)(10)
vero = treedata.Rows(i)(6)
Dim myPen As New System.Drawing.Pen(System.Drawing.Color.Green)
Dim formGraphics As System.Drawing.Graphics
myPen.SetLineCap(LineCap.RoundAnchor, LineCap.ArrowAnchor, DashCap.Flat)
formGraphics = Form8.CreateGraphics()
formGraphics.DrawLine(myPen, Convert.ToSingle(posofmodif), Convert.ToSingle(vero), Convert.ToSingle(hor), Convert.ToSingle(vern))
myPen.Dispose()
formGraphics.Dispose()
Next
However I would like to use labels instead of TextBoxes because it makes no sense to use heavier TextBoxes in this case. But when I simply replace
Dim tb As New TextBox
by
Dim tb As New Label
the labels do appear on the Form as expected but the lines connecting them appear only for a moment and then turn invisible.
I first thought that the problem might be caused by labels being over or below the lines but even when I make sure that no line is crossing any label it happens.
Does anyone have an idea what I could do to avoid this?
This is your problem: Form8.CreateGraphics(). That method is volatile, as it creates a Graphics instance that does not survive the scope in which it's used.
You need to be using the Paint event for whatever control on which you intend to draw. The form, the label...whatever that is. The Paint event provides a Graphics object for you to use, and it gets called whenever the drawing needs to be refreshed.
Because the event fires frequently, you need to be mindful of what you do there. Heavy lifting in a Paint handler can slow an app down considerably.

Capture Active Window with Keyboard SendKeys

After experiencing problems with Visual Basic's printform quality, I have decided to take a different approach. I am going to simulate an ALT-PRINTSCREEN keyboard key-press to capture the form in its current state.
However, before this happens, I need to remove certain elements from the form (such as the border), and after getting the image, I need to replace these elements. I am having some timing/synchronization problems with this, as I am noticing that certain elements are still on the image that has been copied to the clipboard.
I figured this was due to the fact that it takes time to process the changes, and time for Windows to process the simulated "send keys", so I set up sleep timers. I am having mixed results. I notice that if I don't replace the elements afterwords, the clipboard image appears correctly. However, if I do replace them, even if I set up a 20 second sleep timer, they do not appear.
Therefore, I know there is some sort of synchronization problem, but I am not sure how to fix it. Here is the code I am using:
'Make these forms invisble so they are not printed
Logo.Visible = False
MenuStrip1.Visible = False
ReportsButton.Visible = False
pnlmain.BorderStyle = 0 'remove the border
'Sleep
System.Threading.Thread.Sleep(1000)
'simulate an ALT and PRINTSCREEN key click to get ONLY the report
SendKeys.Send("%{PRTSC}")
'Sleep
System.Threading.Thread.Sleep(1000)
'Now, get the image from the clipboard
Dim formImage As System.Drawing.Image
formImage = My.Computer.Clipboard.GetImage()
Logo.Visible = True
MenuStrip1.Visible = True
ReportsButton.Visible = True
pnlmain.BorderStyle = 1
Changing the sleep durations does not seem to change much (unless they are very low).
Any suggestions?
EDIT: here is the code for draw to bitmap (after modifying the code above):
formImage = New Bitmap(Me.Width, Me.Height, Me.CreateGraphics())
Me.DrawToBitmap(formImage, New Rectangle(0, 0, Me.Width, Me.Height))
When it is sent to the print action:
Dim g As Graphics = e.Graphics
'transform the form image so it fits the height and width of the paper, with a 5 px margin
Dim res = printSettings.PrinterResolutions
Dim newSizeDestinationPoints As Point() = {
New Point(5, 5),
New Point(841, 5),
New Point(5, 956)
} 'These values are based on A4 sized paper
'g.DrawImage(formImage, 5, 5)
g.DrawImage(formImage, newSizeDestinationPoints)
The low quality is not coming from the stretch.

VB Scale form for screen capture

I am trying to do a screen capture using the code below
Dim area As Rectangle = FormPrintArea.Bounds
Dim areaWidth As Integer = CInt(area.Width * ScaleFactor)
Dim areaHeight As Integer = CInt(area.Height * ScaleFactor)
Dim capture As Bitmap = New Bitmap(areaWidth, areaHeight, PixelFormat.Format32bppArgb)
Dim graph As Graphics = Graphics.FromImage(capture)
Dim ScaledSize As New Size With {
.Height = areaHeight,
.Width = areaWidth
}
graph.CopyFromScreen(area.X, area.Y, 0, 0, ScaledSize, CopyPixelOperation.SourceCopy)
PictureBox1.Image = capture
``
The problem is I can't find ScaleFactor, Everything works perfect as long as "Change the size of text, apps and the other items: 100%" in Windows 10 Display Settings but if it is set differently like 125% (recommended), I lose about 20% of the image. It looks like there is a ScaleFactor in the LanguageFont class but I can't seem to access it from VB (or C#).
The application has a VB Form (FormPrintArea) that the user uses to define the print area. If I set ScaleFactor to 1.25 on systems set to 125% (recommended), then everything works. Is there any way to get the value from Windows through an API?
I needed to declare my App DPIaware as #Hans Passant said, the easiest way is to add the lines below to the Partial Class startup form (Form1).
<DllImport("User32.dll")>
Private Shared Sub SetProcessDPIAware()
End Sub
Then change the New Sub at the end of the startup form to call SetProcessDPIAware
Public Sub New()
SetProcessDPIAware()
InitializeComponent()
End Sub
Once you do that you can get the ScaleFactor in Form1.Load as follows
DPIScalingX = Me.CreateGraphics().DpiX / 96
DPIScalingY = Me.CreateGraphics().DpiY / 96

How to get Winforms Panel to correctly layout many items?

I've got a WinForms Panel control which holds a large number of child controls. Each child is left docked, causing the horizontal width of the contents to grow. The containing Panel has its AutoScroll property set so that you can get to all the contents.
I'm running into a problem when the total width of the contents gets too large. Once you've hit this maximum width, additional content elements are placed on top of existing contents instead of being placed to the right. But, if I resize the Panel after it has done its initial layout, it corrects itself by expanding its logical width and placing each content element in the correct location. How do I get it to layout correctly before the user resizes the window?
Here's a simple example:
Form1.vb
Public Class Form1
Protected Overrides Sub OnLoad(e As EventArgs)
MyBase.OnLoad(e)
For i As Integer = 1 To 200
Dim gb As New GroupBox
gb.Text = "Box " & i.ToString
gb.Width = 250
gb.Dock = DockStyle.Left
Panel1.Controls.Add(gb)
gb.BringToFront()
Next
End Sub
End Class
Form1.Designer.vb
Partial Class Form1
Inherits System.Windows.Forms.Form
Private Sub InitializeComponent()
Me.Panel1 = New System.Windows.Forms.Panel()
Me.SuspendLayout()
'
'Panel1
'
Me.Panel1.AutoScroll = True
Me.Panel1.Dock = System.Windows.Forms.DockStyle.Fill
Me.Panel1.Location = New System.Drawing.Point(0, 0)
Me.Panel1.Name = "Panel1"
Me.Panel1.Size = New System.Drawing.Size(284, 262)
Me.Panel1.TabIndex = 0
'
'Form1
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(284, 262)
Me.Controls.Add(Me.Panel1)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub
Friend WithEvents Panel1 As System.Windows.Forms.Panel
End Class
This is what the window looks like when it first comes up, scrolled nearly to the end so you can see the problem area. Notice that Box 183 to 199 are missing because they are placed on top of each other. This is not right.
This is what the window looks like after you manually resize it, scrolled nearly to the end. The panel fixed itself in response to the resize; the total logical width of the panel was automatically extended enough to hold all the contents. This is what I want it to look like when it first comes up.
I've tried manually setting the location of each box, and I've tried calling PerformLayout() and several other functions. Nothing seems to work. So far I haven't found the magic combination to get the good layout. Does anyone know how to fix this?
Edit:
Here's a screenshot that might make the issue more obvious. I adjusted the box widths and the number of boxes to show the problem better. See how the last box overlaps box 656? Every box from 657 to 700 has the same incorrect location. Turning off docking and setting the location myself doesn't help.
Looks like a bug with the scrolling information. If you call PerformLayout when the Panel is scrolled all the way to the right, it correctly places the controls in the proper place. That requires some code in the OnShown method:
Protected Overrides Sub OnLoad(e As EventArgs)
MyBase.OnLoad(e)
Panel1.AutoScroll = True
Panel1.SuspendLayout()
For i As Integer = 1 To 200
Dim gb As New GroupBox
gb.Text = "Box " & i.ToString
gb.Width = 250
gb.Dock = DockStyle.Left
Panel1.Controls.Add(gb)
gb.BringToFront()
Next
Panel1.ResumeLayout(False)
End Sub
Protected Overrides Sub OnShown(e As EventArgs)
MyBase.OnShown(e)
Panel1.AutoScrollPosition = New Point(Panel1.HorizontalScroll.Maximum - _
Panel1.HorizontalScroll.LargeChange, 0)
Panel1.PerformLayout()
Panel1.AutoScrollPosition = Point.Empty
End Sub
Of course, having over 200 container controls on the form is never recommended.
AutoScroll is not AutoPositionMyChildren. From MSDN:
When adding controls programmatically to a form, use the AutoScrollPosition property to position the control either inside or outside of the current viewable scroll area.
If you looped thru the controls, to print their location, you's see at some point (probably around 130) that Location.Y becomes fixed at 32767 probably some default unscrolled max. This is also the point they start stacking because they in fact have the same initial location. Some of the code you have makes up for that but it isnt quite right. Once you scroll it, the panel fixes the coords on the child controls.
First, I would suggest that you set Panel1.AutoScrollMinSize to something like {480, 0} so that the HScroll bar appears at design time; this allows you to calc a good height for the boxes which wont cause a VScroll as you add controls.
Dim gb As GroupBox
' only 150 because problem is when (i * width) > 32k
For i As Integer = 0 To 150
gb = New GroupBox
gb.Name = i.ToString ' added
gb.Text = "Box " & i.ToString
gb.Width = 250
' no docking so set the height
gb.Height = Panel1.Bounds.Height - 30 ' trying to avoid the VSCroll
' set location explicitly
gb.Location = NewCtlLocation(Panel1.Controls.Count,
Panel1.AutoScrollPosition.X)
' Dock and Anchor mess up the AutoScroll
'gb.Dock = DockStyle.Left
Panel1.Controls.Add(gb)
' allow panel to update its scroll positions
Panel1.ScrollControlIntoView(gb)
' not needed; seems to offset something with Dock
' changing ZOrder may not always be desirable
'gb.BringToFront()
' debug illumination
Console.WriteLine("{0} {1} {2}", i.ToString,
Panel1.AutoScrollPosition.X.ToString,
gb.Location.X.ToString)
Next
'Go back to start
Panel1.ScrollControlIntoView(Panel1.Controls("0"))
Location helper so you can tweak gutters or margins (dock replacement):
Friend Function NewCtlLocation(ByVal n As Integer,
ByVal ScrollPosX As Integer) As Point
Const TopMargin As Integer = 5
Const LeftMargin As Integer = 5
Return New Point((n * 250) + ScrollPosX, 0)
End Function
Notes:
I have a vertical scroller which repeatedly adds up to 120 user controls which works well but it does not need/use ScrollControlIntoView and they never stack up like yours do. I suspect maybe because they are smaller. There is also at least a second or two before the next one can be added, which may matter. But, good to know.
It might be possible to use the ControlAdded event of the panel to do something, but it would likely amount to ScrollControlIntoView. Doing it once at the end only doesnt work, so using it as they are added is allowing something to get updated as you go.
With the right fiddling, you might be able to get Dock to work, but I suspect it may be part of the problem such as Height and Left set this way dont update the panel's internal scroll map.
Your boxes actually look narrower than 250 - is autosize on?
Suspend/Resume Layout hurt rather than help - they prevent the control from doing anything about the virtual area being populated. It should happen fast enough that no one will see anything. Results:
Works on My SystemTM

Finding the center of application - VB.net

I am wanting to find the center of my application. Please read before saying Me.Width / 2..
I am loading a web page that has a Java based app in it and am wanting to automatically click the, 'login', button once the page has loaded. The VB app is a set size, but will not be maximized allowing the user to move the app around the screen. What I was thinking was doing something like..
SetCursorPos({CenterOfApp.Width} + 20, {CenterOfApp.Height})
..and then simulating a mouse click.
Help would be appreciated, thanks!
Edit 1 - I believe the answer could be below, I am just not sure of the formula required. Again I say, this is a fixed sized VB.net application loading a fixed sized Java application threw the VB.net browser control. The button will always be in the same location of the VB.net app, but the VB.net app will not always be in the same location of the computer screen. Meaning, I will need to find the center of the VB.net app depending on where it is on the screen of the computer.
Dim ScreenWidth As Integer = Screen.PrimaryScreen.Bounds.Width
Dim ScreenHeight As Integer = Screen.PrimaryScreen.Bounds.Height
Dim ClientLeft As Integer = Me.Location.X
Dim LeftOfScreenToApp As Integer ' This is to be the distance from the left side of the screen to the left side of the VB app.
LeftOfScreenToApp = ...
SetCursorPos(LeftOfScreenToApp + (Me.Width / 2) + 20, Me.Height / 2)
So happens that I was just overlooking the fact that Me.Location.X is the offset from left of screen to the left of the VB.net app, so..
Dim LeftOfScreenToLeftOfApp As Integer = Me.Location.X
Dim MiddleOfApp As Integer = LeftOfScreenToLeftOfApp + Me.Width / 2
..gets the center of the app no matter where it is located on the screen.