I have a project which consists of two forms, MainForm and CrossCorrPlotForm. On CrossCorrPlotForm, I have two charts (CrossCorrExpChart and CrossCorrRefChart) which I enabled scroll wheel zooming on, using the code described here and here.
Everything was working perfectly fine, until I added another chart (histChart), on MainForm, which is updated on each frame incoming from a camera (15-20 FPS), using a BackgroundWorker to collect and plot the data from the images. Now, both my charts on CrossCorrPlotChart are not zoomable anymore.
I presume this has something to do with the live-updating chart taking back the focus on each update. I tried adding histChart.Focus = false in the code but to no avail, as it seems "Control.Focused is read-only".
Does anyone have an idea how to make my charts zoomable again ?
Thanks
EDIT : Here is the code for the BackgroundWorker that updates chartHist :
private void OnFrameReceived(Frame frame)
{
bgw1.RunWorkerAsync(frame);
}
private void bgw1_DoWork(object s, DoWorkEventArgs e)
{
Frame frame = (Frame)e.Argument;
myBitmap = null;
try
{
frame.Fill(ref myBitmap);
mycamera.QueueFrame(frame);
SaveBitmap = myBitmap.Clone(cloneRect, myBitmap.PixelFormat);
BitmapToPrint = myBitmap.Clone(cloneRect, myBitmap.PixelFormat);
}
catch {}
}
private void bgw1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (this.InvokeRequired)
{
BeginInvoke((Action)(() => bgw1_RunWorkerCompleted(sender, e)));
}
else
{
new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
DataFromBitmap DataFromBitmapHist = new DataFromBitmap(SaveBitmap.Clone(cloneRect, SaveBitmap.PixelFormat));
PixelColorCount = DataFromBitmapHist.ColorCountOutput();
}).Start();
chartHist.Titles["Title2"].Visible = false;
chartHist.Series["Pixel count"].Points.Clear();
//Plotting the pixel counter, to detect saturation
for (int i = 0; i < PixelColorCount.Length; i++)
{
chartHist.Series["Pixel count"].Points.AddXY(i, PixelColorCount[i]);
}
//If there are saturated pixels : toggle a title on chartHist to warn the user
if (PixelColorCount.Last() > 1)
{
chartHist.Titles["Title1"].Visible = false;
chartHist.Titles["Title2"].Visible = true;
}
else
{
chartHist.Titles["Title1"].Visible = true;
chartHist.Titles["Title2"].Visible = false;
}
}
}
NOTES :
OnFrameReceived is a function from the API of the camera, it contains code that fires when a Frame is received from the camera.
frame.Fill puts the image contained in the Frame object in a Bitmap.
mycamera.QueueFrame sends back the Frame object to the camera, to receive a new image.
I had to use multiple threads, because using the UI thread too much resulted in blocking the reception from the camera.
Related
I have some code below that can take the current Image and displaying it whenever I click a PushSnap Button. How can I go about having it continuously capturing and displaying the updated image in an interval (say 100 ms)
private void PushSnap_Click(object sender, EventArgs e)
{
if (mycam == null)
{
MessageBox.Show("Internal Error: mycam is null");
return; // internal error
}
string text = "";
if (IsMyFormStatus_Opened())
{
if (!mydcam.buf_alloc(3))
{
MessageBox.Show("Frame allocation failed");
return;
}
}
// start acquisition
mycam.m_capmode = CAMCAP_START.SNAP; //one time capturing where Acquisition will start after m_nFrameCount frames
if (!mycam.cap_start())
{
return;
}
MyFormStatus_Acquiring();
MyThreadCapture_Start();
}
The Following MyThreadCapture_Start()
private void MyThreadCapture_Start()
{
m_threadCapture = new Thread(new ThreadStart(OnThreadCapture));
m_threadCapture.IsBackground = true;
m_threadCapture.Start();
}
and the following OnThreadCapture()
private void OnThreadCapture()
{
using (mycamwait = new MycamWait())
{
while (True)
{
CAMWAIT eventmask = CAMWAIT.CAPEVENT.FRAMEREADY | CAMWAIT.CAPEVENT.STOPPED;
DCAMWAIT eventhappened = DCAMWAIT.NONE;
if (mycamwait.start(eventmask, ref eventhappened))
{
if (eventhappened & CAMWAIT.CAPEVENT.FRAMEREADY)
{
int NewestFrame = 0;
int FrameCount = 0;
if (mycam.cap_transferinfo(ref NewestFrame, ref FrameCount))
{
MyUpdateImage(iNewestFrame);
}
}
if (eventhappened & CAMWAIT.CAPEVENT.STOPPED)
{
bContinue = false;
if (m_cap_stopping == false && mycam.m_capmode == CAMCAP_START.SNAP)
{
//cap_stop() happens automatically, therefore update the main dialog
//MySnapCaptureFinished();
}
}
}
}
}
Afterwards the Display is updated in the following way
private void UpdateDisplay()
{
Image oldImg = PicDisplay.Image;
if (m_bitmap != null)
{
// Show center of image
Int32 y0 = (m_image.height - PicDisplay.Height) / 2;
Int32 x0 = (m_image.width - PicDisplay.Width) / 2;
Rectangle rc = new Rectangle(x0, y0, PicDisplay.Width, PicDisplay.Height);
Bitmap bmp = new Bitmap(PicDisplay.Width, PicDisplay.Height, PixelFormat.Format24bppRgb);
using (var gr = Graphics.FromImage(bmp))
{
gr.DrawImage(m_bitmap, 0, 0, rc, GraphicsUnit.Pixel);
}
PicDisplay.Image = bmp;
PicDisplay.Refresh();
}
else
{
PicDisplay.Image = null;
}
if (oldImg != null)
oldImg.Dispose();
}
I would really recommend using Tasks instead of Threads. That should let you update your methods to take parameters and return results:
Image CaptureImage(); // I.e. similar code to OnThreadCapture
UpdateDisplay(Image);
And capture an image like:
public async void PushSnap_Click(){
...
try{
var image = await Task.Run(CaptureImage);
DisplayImage(image);
}
catch{
// Handle exceptions
}
That should let you simply rewrite your capture code to use a loop if you want live capture:
while(showLiveImagesBool){
var image = await Task.Run(CaptureImage);
DisplayImage(image);
}
The await-part should ensure the UI thread is not blocked while waiting for an image to be captured. If you do not want to show every image you might use a timer instead of a loop, just pick a timer that runs on the UI thread.
Keep in mind that showing live images will require a bit more performance, so you might need to optimize display and/or capture code. Ideally you should reuse image buffers when doing the capturing and displaying, otherwise you will allocate a fair amount of large objects that require a 2 gen GC to clean up.
I need to save an image to disk that came from a web-cam between 5 and 10 seconds ago from when the "save" command comes in via serial port.
To get there, I have the webcam going into a pictureBox.Image (using opencv4), and then 2 Bitmap variables. Every 5 seconds a timer ticks, and Stored_bitmap_2 = Stored_bitmap_1, then Stored_bitmap_1 = (bitmap) pictureBox.Image.
When the right serial command comes in, I try to
Stored_image_2.Save("C:\Users\GreenWorld\Desktop\test.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
and I get a run-time error of invalid parameter.
When I do the same thing in a stand-alone project with some buttons (inside the button-click event handler), it works every time.
When I do this inside the serialPort_DataReceived handler, I get all kinds of cross-thread errors. So, I moved the save attempt to its own subroutine, and that fixed that but now this.
I am by no means a professional programmer, I'm an engineer with a simple problem and I can usually write a little simplistic code to fix my immediate issue. Please go easy on me in the explanation :-)
Sample code:
using OpenCvSharp;
using OpenCvSharp.Extensions;
namespace Weld_picture
{
public partial class Form1 : Form
{
// Create class-level accessible variables
int Welding_camera_ID = 1;
VideoCapture capture;
Mat frame;
Bitmap image;
private Thread camera;
bool isCameraRunning = false;
string Serial_command = "";
Bitmap Stored_image_1;
Bitmap Stored_image_2;
bool Image_saved = false;
string Station_ID = "GWM-PWS-01";
string File_name = "";
private void CaptureCamera() // from someone else's sample code
{
camera = new Thread(new ThreadStart(CaptureCameraCallback));
camera.Start();
}
private void CaptureCameraCallback() // from someone else's sample code
{
frame = new Mat();
capture = new VideoCapture(Welding_camera_ID);
capture.Open(Welding_camera_ID);
if (capture.IsOpened())
{
while (isCameraRunning)
{
capture.Read(frame);
image = BitmapConverter.ToBitmap(frame);
if (pictureBox1.Image != null)
{
pictureBox1.Image.Dispose();
}
pictureBox1.Image = image;
}
}
}
public Form1()
{
InitializeComponent();
SerialPort1.Open();
}
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
string dummy;
char CR = (char)0x0D;
while (SerialPort1.BytesToRead > 0)
{
Serial_command += (char)SerialPort1.ReadByte();
}
while (Serial_command.IndexOf(CR) > 0)
{
dummy = Serial_command.Substring(0, Serial_command.IndexOf(CR));
Serial_command = Serial_command.Substring(Serial_command.IndexOf(CR) + 1, (Serial_command.Length - (Serial_command.IndexOf(CR) + 1)));
Serial_command.Trim();
dummy.Trim();
proc_Process_serial_data(dummy);
}
}
//*************************************************************************************************************************************
/* the first timer is a 5-second interval. It's the "memory" function so that if/when the save-to-disk is triggered I can store the last-time-shutter-open image */
//*************************************************************************************************************************************
private void Timer_picture_interval_Tick(object sender, EventArgs e)
{
checkBox1.Checked = !checkBox1.Checked;
Timer_picture_interval.Stop();
Stored_image_2 = Stored_image_1;
Stored_image_1 = (Bitmap) pictureBox1.Image;
Timer_picture_interval.Start();
}
//*************************************************************************************************************************************
// the second timer is a 30-second interval. It's the way to turn capture off if the PLC/camera box somehow goes off-line
//*************************************************************************************************************************************
private void Timer_camera_powerdown_Tick(object sender, EventArgs e)
{
if (isCameraRunning)
capture.Release();
isCameraRunning = false;
Timer_picture_interval.Stop();
}
//*************************************************************************************************************************************
private void proc_Process_serial_data(string Serial_string)
{
if (Serial_string.IndexOf("Still here") > 0)
{
if (!isCameraRunning)
CaptureCamera();
isCameraRunning = true;
}
if (Serial_string.IndexOf("Sun's up") > 0)
{
Timer_picture_interval.Start();
Timer_camera_powerdown.Start();
}
if (Serial_string.IndexOf("It's dark") > 0)
{
if ((Stored_image_2 != null) && (!Image_saved)) // in case there's 2 subsequent requests to save the same thing (weld stutter)
{
File_name = "C:\\Users\\GreenWorld\\Desktop\\" + Station_ID + " D" + DateTime.Now.ToString("yyyy_MM_dd THH_mm_ss") + ".jpg";
Stored_image_2.Image.Save("C:\\Users\\GreenWorld\\Desktop\\test.bmp" , System.Drawing.Imaging.ImageFormat.Bmp );
Image_saved = true;
Timer_picture_interval.Stop();
}
Timer_camera_powerdown.Start();
}
}
}
}
You likely be able to fix the cros-threading error by simply calling your proc_Process_serial_data() method within an Invoke() call.
Change:
proc_Process_serial_data(dummy);
To:
this.Invoke((MethodInvoker)delegate ()
{
proc_Process_serial_data(dummy);
});
Also, these two lines aren't actually doing anything:
Serial_command.Trim();
dummy.Trim();
To Trim() the strings, you have capture the returned strings and re-assign them to the original variables:
Serial_command = Serial_command.Trim();
dummy = dummy.Trim();
I am currently using liveChart to plot a real time graph of 3 values: a position, a load and a deformation. The program is based on the Doli.DoPE library (a proprietary dll)
In MainForm.cs, there is an event that is triggered everytime the sensor records a new value (every millisecond or so).
public void Initialisation()
{
//...
MyEdc.Eh.OnDataHdlr += new DoPE.OnDataHdlr(OnData)
//...
}
with
private int OnData(ref DoPE.OnData Data, object Parameter)
{
DoPE.Data Sample = Data.Data;
if (Data.DoPError == DoPE.ERR.NOERROR)
{
Int32 Time = Environment.TickCount;
if ((Time - LastTime) >= 250 /*ms*/)
{
// Send the data from the ondata handler inside of a global list
ListData.time.Add(Sample.Time);
ListData.position.Add(Sample.Sensor[(int)DoPE.SENSOR.SENSOR_S]);
ListData.load.Add(Sample.Sensor[(int)DoPE.SENSOR.SENSOR_F]);
ListData.extend.Add(Sample.Sensor[(int)DoPE.SENSOR.SENSOR_E]);
Thread ThForUpdateChart = new Thread(() =>
{
if (NewINstanceOfChart != null)
{ NewINstanceOfChart.UpdateValues(ListData.time.Last(), ListData.position.Last(),ListData.load.Last(), ListData.extend.Last()); }
});
ThForUpdateChart.Start();
LastTime = Time;
}
}
return 0;
}
The function UpdateValues is part of a second form RealTimeChart.cs called in the MainForm through a button click event:
private void btnGraph_Click(object sender, EventArgs e)
{
var thread = new Thread(() =>
{
NewINstanceOfChart = new RealTimeChart(ListData);
NewINstanceOfChart.Show();
});
thread.Start();
}
the form RealTimeCharts.cs is initalised this way:
public RealTimeChart(Globals ListData)
{
InitializeComponent();
//measures = ListData;
ListPosition = new ChartValues<ObservablePoint>();
for (int i = 0; i < measures.load.Count(); i++)
{
ListPosition.Add(new ObservablePoint
{
X = measures.time[i],
Y = measures.position[i]
});
}
ListLoad = new ChartValues<ObservablePoint>();
for (int i = 0; i < measures.load.Count(); i++)
{
ListLoad.Add(new ObservablePoint
{
X = measures.time[i],
Y = measures.load[i]
});
}
ListExtend = new ChartValues<ObservablePoint>();
for (int i = 0; i < measures.load.Count(); i++)
{
ListExtend.Add(new ObservablePoint
{
X = measures.time[i],
Y = measures.extend[i]
});
}
resultChart.Series.Add(new LineSeries
{
LineSmoothness = 0,
Values = ListPosition,
PointGeometrySize = 2,
StrokeThickness = 4
});
SetXAxisLimits();
}
And the UpdateValues function is defined as followed:
public void UpdateValues(double time, double position, double load, double extend)
{
measures.time.Add(time-measures.TareTime);
measures.position.Add(position);
measures.load.Add(load);
measures.extend.Add(extend);
UpdateEnabledSequencialPartToTrue();
}
public void UpdateEnabledSequencialPartToTrue()
{
if (this.InvokeRequired)
BeginInvoke(new System.Action(() => this.InternalUpdateEnabledSequencialPartToTrue()));
else
InternalUpdateEnabledSequencialPartToTrue();
}
private void InternalUpdateEnabledSequencialPartToTrue()
{
try
{
ListPosition.Add(new ObservablePoint
{
X = measures.time.Last(),
Y = measures.position.Last()
});
ListLoad.Add(new ObservablePoint
{
X = measures.time.Last(),
Y = measures.load.Last()
});
ListExtend.Add(new ObservablePoint
{
X = measures.time.Last(),
Y = measures.extend.Last()
});
//LineSeries plot = new LineSeries();
SetXAxisLimits();
// lets only use the last 14400 values (1h long recording, 14400 values at frequency of 1 record very 250ms, see OnData function MainForm
if (measures.time.Count > 14400)
{
ListPosition.RemoveAt(0);
ListLoad.RemoveAt(0);
ListExtend.RemoveAt(0);
}
}
catch (NullReferenceException) { }
}
After a minute, the programme starts to be really laggy. I tried putting the second winform (RealTimeCharts) on another thread so the MainForm does not lag (it is piloting a machine, it has to be responsive), but no success.
I would like to know if the whole thing is laggy because the code is way too bad, or if it is liveChart that reached its (free) limits. Would you advice another way to plot real time data ?
In MainForm.cs, there is an event that is triggered everytime the sensor records a new value (every millisecond or so).
That is natturally way higher then what Winforms Drawing can take. See, drawing a GUI is expensive. If you only do it once per user-triggered event, you will never notice that. But do it from a loop - including sampling a sensor every MS - and you can quickly overlord the UI. My first Multithreading tests actually appeared to have failed on big numbers, becaus I ended up sending so many updates I plain overloaded the GUI thread. Since then I know not to go past progress bars.
You can add data to a background collection as quickly as you can sample them, but you can not draw that quickly. And honestly drawing more often then 30-60 times/second (every ~17 ms) is not really going to help anyone anyway. Usually you can not use a timer, as the Tick might happen more often then it can be processed - again, a GUI Thread with a overflowing Event Queue.
I do not have any rate limiting code for WindowsForms. But I would guess an Event that re-queues itself at the end of the EventQueue after finishing the work would work.
I have a form, this form has a panel. When the form OnShown fires, I start a background worker to execute a long running code method a set number of times. In this background worker, I add a User Control to the panel one-by-one/per-iteration while displaying a Wait picture on the UI thread.
Problem: My long running code runs faster than the panel can draw the controls I've added to it. Basically, when my form loads, it takes about 1.5 seconds to run the code (I can tell by how fast the wait picture hides itself), however, the child controls on the panel take between 4 and 6 seconds to draw themselves...
As you can imagine this would cause some major confusion to the user in that the Wait image would hide/the code would be done, but the controls wouldn't be shown for another 4 seconds.
So, is there a way to essentially "Wait" until all the child controls on the panel are drawn, so that i can ONLY THEN hide the Wait picture through some mechanism in the Panel control?
If that's not possible, keep in mind that I'm using a User Control and have access to those events as well. So if there's something there I could use to fire AFTER the User Control has drawn itself (or thinks it has), maybe i could use that instead and count until I reach the known count?
Here's my code, obscured a bit to not show any work info/not tell you what I'm doing but should be enough for you to find out if there's something wrong with the code itself...
Legend:
SW() = Show Waiting Picture,
HW() = Hide Waiting Picture,
this.wait = Wait Picture Control with progress bars...
private void MyForm_Shown(object sender, EventArgs e)
{
if (this.SomeList.Count <= 0 || this.SomeObject == null)
this.DialogResult = DialogResult.Abort;
SW();
this.wait.Text = "Loading everything...";
this.wait.TotalBar_Max = this.SomeList.Count;
this.wait.TotalBar_Value = 0;
this.wait.CurrentBar_Max = 100;
BackgroundWorker bkg = new BackgroundWorker();
bkg.DoWork += new DoWorkEventHandler(bkg_DoWork);
bkg.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bkg_RunWorkerCompleted);
bkg.RunWorkerAsync();
}
void bkg_DoWork(object sender, DoWorkEventArgs e)
{
foreach (SomeType SomeItem in this.SomeList)
{
//Update Progress bar...
this.Invoke(new MethodInvoker(delegate
{
this.wait.Text = "Loading Specifics for: " + SomeItem.ToString();
this.wait.CurrentBar_Value = 50;
}));
MyControl tmp = new MyControl();
tmp.Name = SomeItem.ToString();
tmp.Text = SomeItem.ToString() + " - " + this.SomeObject.Name;
ServerWorker work = new ServerWorker(SomeItem);
Dictionary<string, List<string>> test = work.LongRunningCode();
//fill in/Init User Control based on long running code results...
if (test["Key1"] != null && test["Key1"].Count > 0)
{
test["Key1"].Sort();
tmp.Key1 = test["Key1"];
}
else
{
tmp.Key1_Available = false;
}
if (test["Key2"] != null && test["Key2"].Count > 0)
{
test["Key2"].Sort();
tmp.Key2 = test["Key2"];
}
else
{
tmp.Key2_Available = false;
}
if (test["Key3"] != null && test["Key3"].Count > 0)
{
test["Key3"].Sort();
tmp.Key3 = test["Key3"];
}
else
{
tmp.Key3_Available = false;
}
//Add user control, and update progress bars...
this.Invoke(new MethodInvoker(delegate
{
if (this.panel1.Controls.Count <= 0)
{
tmp.Top = 5;
tmp.Left = 5;
}
else
{
tmp.Top = this.panel1.Controls[this.panel1.Controls.Count - 1].Bottom + 5;
tmp.Left = 5;
}
this.panel1.Controls.Add(tmp);
this.wait.Text = "Loading Specifics for: " + SomeItem.ToString();
this.wait.CurrentBar_Value = 100;
this.wait.TotalBar_Value += 1;
this.panel1.Refresh();
}));
}
e.Result = true;
}
void bkg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
HW();
}
I found my own solution here:
https://stackoverflow.com/a/6653042/1583649
By building a List within the background worker instead of Invoking the UI thread, and then Panel.Add() on the RunWorkerCompleted event instead, the Wait image was able to stay alive until all the controls were actually drawn on the Panel.
I need to write a code which moves a picture1 to the top of the screen after a click of a button. After pictire1 reaches the top 20 pixels of the screen must become invisible and make picture2 visible. This is my wrong code:
private void button1_Click(object sender, EventArgs e)
{
int b = pictureBox1.Top;
for (int i = b; i < 20; i--)
{
pictureBox1.Top = i;
System.Threading.Thread.Sleep(20);
}
if (pictureBox1.Top < 20)
{
pictureBox1.Visible = false;
pictureBox2.Visible = true;
}
}
Any ideas how can it be fixed?
This seems wrong:
for (int i = b; i < 20; i--)
{
pictureBox1.Top = i;
System.Threading.Thread.Sleep(20);
}
This would loop while i < 20. From your code, however, one can see that if Top == 20 is reached, you'd like to show another picture. So I guess that should be i >= 20 and the if should be Top <= 20.
Also, you are not refreshing your display. Due to the loop and the Sleep, there will be no UI updates.
Add this, as Cobra_Fast suggests before the Sleep:
this.Invalidate();
this.Refresh();
To sum it up, the following should work (I've also slightly modified the code to make it clearer):
private void button1_Click(object sender, EventArgs e)
{
while (pictureBox1.Top >= 20)
{
pictureBox1.Top = pictureBox1.Top - 1;
this.Invalidate();
this.Refresh();
System.Threading.Thread.Sleep(20);
}
// Here you KNOW that the picture box is at y-position 20, so there's not need
// for the IF
pictureBox1.Visible = false;
pictureBox2.Visible = true;
}
The problem with the above code is that it blocks the UI. To keep it responsive, I'd use a timer as follows:
private void button1_Click(object sender, EventArgs e)
{
// Create a threaded timer
System.Timers.Timer animationTimer = new System.Timers.Timer();
animationTimer.Interval = 20;
animationTimer.AutoReset = false; // Only one Ping! We'll activate it if necessary
animationTimer.Elapsed += new ElapsedEventHandler(AnimationStep);
animationTimer.Start();
// Disable the button also, because we don't want another timer instance to
// interfere with our running animation
button1.Enabled = false;
}
Then, create the event that's called when the timer fires:
private void AnimationStep(object source, ElapsedEventArgs e)
{
// The following code needs to be executed in the context of the UI thread.
// We need to use this.Invoke in Forms or this.Dispatcher.Invoke in WPF
this.Invoke((Action)delegate()
{
// Move picture. Note that we don't need to update the display here
// because the UI thread gets time to do its work while the timer waits
// to fire below
if (pictureBox1.Top > 20)
pictureBox1.Top--;
// Show other picture maybe. I use <= here because initially, the
// position of the picture box may already smaller than 20.
if (pictureBox1.Top <= 20)
{
pictureBox1.Visible = false;
pictureBox2.Visible = true;
}
// Or let the timer fire again if we still need to animate
else
{
(source as System.Timers.Timer).Start();
}
}
}
This works as follows: A timer is created that fires once after 20ms. It then moves the picture up one pixel and then either shows the other picture if the animation is finished or starts the timer again for moving the picture up another pixel.
This keeps the UI responsive and still allows you to animate your picture. The downside is, the animation may not be as smooth as you might want it to be in case your windows is moved or other "work" needs be done by the UI thread.
What if initially b is greater than 20? The loop won't run because the condition in the loop is that i < 20. Because the loop is not ran, nothing will ever happen.
Consider changing the condition in your loop. You probably want the picture to move up, as you're reducing the Top-property. Let's say that initially the Top of pictureBox1 is 40. Following code will work:
while(pictureBox1.Top >= 20)
{
pictureBox1.Top--;
System.Threading.Thread.Sleep(20);
Invalidate();
Refresh();
}
Since the Top is now less than 20, the if-statement can be omitted and you can just call:
pictureBox1.Visible = false;
pictureBox2.Visible = true;
Complete code:
while(pictureBox1.Top >= 20)
{
pictureBox1.Top--;
System.Threading.Thread.Sleep(20);
Invalidate();
Refresh();
}
pictureBox1.Visible = false;
pictureBox2.Visible = true;
After the pictureBox1.Top assignment, call:
this.Invalidate();
this.Refresh();
Assuming you're working with WinForms.