In this blog post I will share with you the steps that I took to create myself a WebCam Monitor program using Visual Studio 2022 and C# .NET 6.0 (although I believe that this same code might work OK with some older versions of .NET as well).
The reason why I developed this particular WebCam Monitor program, is because I want to take time-lapse snapshot images of a flower pot plant that I have sitting next to a bay window, so that I can see its leaves move over a period of time over the course of a couple of days (see below).
The WebCam that I am using for this project is a USB Logitech Webcam, that is connected to a Desktop PC that is running Windows 10 operating system.
To begin, I performed the following steps on my development computer to create a new blank C# Windows Forms project:
Once the Create a new project wizard finishes creating the initial project and loads it into Visual Studio's editor, I right mouse click on the project name from Solution Explorer and then select Add and Class... from the popup context menu to create and add a new Class file to the project. I named this new class file settingsData.cs:
The settingsData.cs data class will be used to load and save any program user settings for this application (i.e. window size, location, last used form settings, etc...) into an external XML file. I include this so that the last used form values can automatically be reapplied and set the next time the user re-uses this application, so that the user does not have to manually reset the form values every time they re-use the application (added value by providing users convenience).
The following is the complete source code of settingsData.cs:
// Copyright 2024 T&J Divisions, LLC
// Designed and developed by Tim Tolbert
// All Rights Reserved
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
namespace WebCamMonitor
{
[Serializable ]
[XmlRoot ("settings" )]
public class SettingsData
{
[XmlIgnore ]
public string filePath = Path .GetDirectoryName (System.Reflection.Assembly .GetExecutingAssembly ().Location) + "\\settings.xml" ;
[XmlElement ]
public int left { get ; set ; }
[XmlElement ]
public int height { get ; set ; }
[XmlElement ]
public int top { get ; set ; }
[XmlElement ]
public int width { get ; set ; }
[XmlElement ]
public int saveInterval { get ; set ; }
[XmlElement ]
public int intervalType { get ; set ; }
[XmlElement ]
public string savePath { get ; set ; }
public void initFrom (SettingsData ? copy)
{
// set default values
this .left = 100;
this .height = 450;
this .top = 100;
this .width = 850;
this .saveInterval = 1;
this .intervalType = 0;
this .savePath = "C:\\temp\\webcam\\" ;
// init from copy, if present
if (copy != null )
{
this .left = copy.left;
this .height = copy.height;
this .top = copy.top;
this .width = copy.width;
this .saveInterval = copy.saveInterval;
this .intervalType = copy.intervalType;
this .savePath = copy.savePath;
}
}
public SettingsData ()
{
// init with default values
initFrom (null );
}
public void initFrom (object copy)
{
try
{
// try and init object as SettingsData
initFrom ((SettingsData )copy);
}
catch
{
// if error then just init with default values
initFrom (null );
}
}
public bool initFromXMLFile ()
{
return initFromXMLFile (this .filePath);
}
// init this data object from an xml file
public bool initFromXMLFile (string ? filename)
{
// init return value
bool functionResult = false ;
// check if null
if(filename == null )
{
filename = this .filePath;
}
// try and load from file
try
{
if (File .Exists (filename))
{
XmlSerializer mySerializer = new XmlSerializer (this .GetType());
FileStream fs = new FileStream (filename, FileMode.Open);
object copy = (object )mySerializer.Deserialize (fs);
functionResult = true ;
fs.Close ();
this .initFrom (copy);
}
else
{
functionResult = false ;
}
}
catch
{
functionResult = false ;
}
return functionResult;
}
public void saveToXMLFile ()
{
saveToXMLFile (this .filePath, true );
}
// serialize and save this data class object to an XML file
public void saveToXMLFile (string filename, bool overwrite)
{
// pre-check
if (filename.Trim () == "" )
{
throw new Exception("No filename was specified! Please provide a valid file name first!" );
}
if (File.Exists (filename) == true )
{
if (overwrite == true )
{
File .Delete (filename);
}
else
{
throw new Exception("File already exists! Set the overwrite flag to true to overwrite it!" );
}
}
// save to file
XmlSerializer mySerializer = new XmlSerializer (this .GetType());
TextWriter writer = new StreamWriter (filename);
mySerializer.Serialize (writer, this );
writer.Close ();
}
}
}
I then right mouse click on the project name again, and add another new Class file to the project. I name this new class file TCamDevice.cs. This class will be used to represent the main WebCam object itself.
The following is the complete source code for TCamDevice.cs:
// Copyright 2024 T&J Divisions, LLC
// Designed and developed by Tim Tolbert
// All Rights Reserved
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace WebCamMonitor
{
public class TCamDevice
{
private const short WM_CAP = 0x400;
private const int WM_CAP_DRIVER_CONNECT = 0x40a;
private const int WM_CAP_DRIVER_DISCONNECT = 0x40b;
private const int WM_CAP_EDIT_COPY = 0x41e;
private const int WM_CAP_SET_PREVIEW = 0x432;
private const int WM_CAP_SET_OVERLAY = 0x433;
private const int WM_CAP_SET_PREVIEWRATE = 0x434;
private const int WM_CAP_SET_SCALE = 0x435;
private const int WM_CAP_START = 0x400;
private const int WM_CAP_SEQUENCE = WM_CAP_START + 62;
private const int WM_CAP_FILE_SAVEAS = WM_CAP_START + 23;
private const int WM_CAP_FILE_SAVEDIB = WM_CAP_START + 25;
private const int WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
private const int WS_CHILD = 0x40000000;
private const int WS_VISIBLE = 0x10000000;
[DllImport ("avicap32.dll" )]
protected static extern int capCreateCaptureWindowA ([MarshalAs(UnmanagedType.VBByRefStr)] ref string lpszWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, int hWndParent, int nID);
[DllImport ("user32" , EntryPoint = "SendMessageA" )]
protected static extern int SendMessage (int hwnd, int wMsg, int wParam, [MarshalAs(UnmanagedType.AsAny)] object lParam);
[DllImport ("user32" )]
protected static extern int SetWindowPos (int hwnd, int hWndInsertAfter, int x, int y, int cx, int cy, int wFlags);
[DllImport ("user32" )]
protected static extern bool DestroyWindow (int hwnd);
int deviceHandle;
int index;
public TCamDevice (int index)
{
this .index = index;
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value ; }
}
private string _version;
public string Version
{
get { return _version; }
set { _version = value ; }
}
public override string ToString ()
{
return this .Name;
}
// To Initialize the device
public void Init (int windowHeight, int windowWidth, int handle)
{
string deviceIndex = Convert.ToString (this .index);
deviceHandle = capCreateCaptureWindowA (ref deviceIndex, WS_VISIBLE | WS_CHILD, 0, 0, windowWidth, windowHeight, handle, 0);
if (SendMessage (deviceHandle, WM_CAP_DRIVER_CONNECT, this .index, 0) > 0)
{
SendMessage (deviceHandle, WM_CAP_SET_SCALE, -1, 0);
SendMessage (deviceHandle, WM_CAP_SET_PREVIEWRATE, 0x42, 0);
SendMessage (deviceHandle, WM_CAP_SET_PREVIEW, -1, 0);
SetWindowPos (deviceHandle, 1, 0, 0, windowWidth, windowHeight, 6);
}
}
// Shows the webcam preview in the control
public void ShowWindow (global ::System.Windows.Forms.Control windowsControl)
{
Init (windowsControl.Height, windowsControl.Width, windowsControl.Handle.ToInt32 ());
}
// Stop the webcam and destroy the handle
public void Stop ()
{
SendMessage (deviceHandle, WM_CAP_DRIVER_DISCONNECT, this .index, 0);
DestroyWindow (deviceHandle);
}
public void StartRecording ()
{
SendMessage (deviceHandle, WM_CAP_SEQUENCE, 0, 0);
}
public void StopRecording (string targetAVIFilePath)
{
SendMessage (deviceHandle, WM_CAP_FILE_SAVEAS, 0, targetAVIFilePath);
}
public Image CaptureImage (int imgWidth = 500, int imgHeight = 500)
{
Bitmap bmp = new Bitmap (imgWidth, imgHeight);
Image img = null ;
Clipboard .Clear ();
SendMessage (deviceHandle, WM_CAP_EDIT_COPY, 0, 0);
IDataObject data = Clipboard .GetDataObject ();
if (data.GetDataPresent (bmp.GetType ()))
{
img = (Image )data.GetData (bmp.GetType ());
}
return img;
}
}
}
For developing the Front-End user interface, I load Form1 into the form editor and conduct the following activities:
To complete the program, I edited Form1.cs source code to instantiate an instance of the WebCam object (TCamDevice), and then I coded each of the form controls callback methods, including user settings during form load and closing, with the code below. I also added a few helper functions for reseting the countdown timer, and for saving the snapshot to file.
The following is the complete source code for Form1.cs:
// Copyright 2024 T&J Divisions, LLC
// Designed and developed by Tim Tolbert
// All Rights Reserved
namespace WebCamMonitor
{
public partial class Form1 : Form
{
private TCamDevice webcam1;
private SettingsData mySettings;
private int totalSnapshots;
private DateTime endTime;
public Form1 ()
{
InitializeComponent ();
}
private void Form1_Load (object sender, EventArgs e)
{
// update form title to show program name and app version
this .Text = Application .ProductName + " ~ Version " + Application .ProductVersion;
// load user settings from XML file
mySettings = new SettingsData ();
mySettings.initFromXMLFile ();
// ensure form will be positioned on screen
if (mySettings.left < 0)
{
mySettings.left = 0;
}
if (mySettings.top < 0)
{
mySettings.top = 0;
}
if (mySettings.width < 100)
{
mySettings.width = 100;
}
if (mySettings.height < 100)
{
mySettings.height = 100;
}
// set form position
this .Left = mySettings.left;
this .Top = mySettings.top;
// set form size
this .Width = mySettings.width;
this .Height = mySettings.height;
// init form controls with last used values
saveInterval.Value = mySettings.saveInterval;
intervalType.SelectedIndex = mySettings.intervalType;
timer1.Interval = mySettings.saveInterval;
textBoxSavePath.Text = mySettings.savePath;
// init webcam
totalSnapshots = 0;
webcam1 = new TCamDevice (0);
webcam1.ShowWindow (pictureBox1);
// init form controls
buttonRecord.BackColor = Color .IndianRed;
buttonRecord.ForeColor = Color .White;
labelCountdown.Text = "" ;
}
private void Form1_FormClosing (object sender, FormClosingEventArgs e)
{
// stop the webcam
webcam1.Stop ();
webcam1 = null ;
// make sure timer is stopped
if (timer1.Enabled == true )
{
timer1.Enabled = false ;
}
// update user settings
mySettings.left = this .Left;
mySettings.top = this .Top;
mySettings.width = this .Width;
mySettings.height = this .Height;
mySettings.saveInterval = (int )saveInterval.Value;
mySettings.intervalType = intervalType.SelectedIndex;
mySettings.savePath = textBoxSavePath.Text;
// save user settings to XML file
mySettings.saveToXMLFile ();
}
private void buttonRecord_Click (object sender, EventArgs e)
{
// toggle the record state
if (buttonRecord.Text == "Start Recording" )
{
// update form controls
buttonRecord.Text = "Stop Recording" ;
buttonRecord.BackColor = Color .Orange;
buttonRecord.ForeColor = Color .Red;
labelCountdown.Text = "" ;
// disable form controls
buttonBrowse.Enabled = false ;
saveInterval.Enabled = false ;
intervalType.Enabled = false ;
textBoxSavePath.Enabled = false ;
// set countdown
resetCountDown ();
// reset snapshot count
totalSnapshots = 0;
// begin timer
timer1.Enabled = true ;
}
else
{
// stop timer
timer1.Enabled = false ;
// enable form controls
buttonBrowse.Enabled = true ;
saveInterval.Enabled = true ;
intervalType.Enabled = true ;
textBoxSavePath.Enabled = true ;
// update form controls
buttonRecord.Text = "Start Recording" ;
buttonRecord.BackColor = Color .IndianRed;
buttonRecord.ForeColor = Color .White;
labelCountdown.Text = "" ;
}
// take snapshot now
saveSnapshot ();
// update snapshot counter
totalSnapshots++;
toolStripStatusLabel1.Text = "Total Snapshots: " + Convert.ToString (totalSnapshots);
}
private void buttonBrowse_Click (object sender, EventArgs e)
{
// browse for a new root path to save snapshot images into
folderBrowserDialog1.SelectedPath = textBoxSavePath.Text;
if (folderBrowserDialog1.ShowDialog () == DialogResult .OK)
{
textBoxSavePath.Text = folderBrowserDialog1.SelectedPath;
// ensure folder path ends with a backslash
if (textBoxSavePath.Text.Trim () == "" )
{
textBoxSavePath.Text = "C:\\temp\\webcam\\" ;
}
else if (textBoxSavePath.Text.EndsWith("\\" ) == false )
{
textBoxSavePath.Text = textBoxSavePath.Text + "\\" ;
}
}
}
private void resetCountDown ()
{
// calculate total number of seconds to count down
int totalSeconds = 1;
if(intervalType.SelectedIndex == 0)
{
// seconds
totalSeconds = (int )saveInterval.Value;
}
else if (intervalType.SelectedIndex == 1)
{
// minutes
totalSeconds = (int )saveInterval.Value * 60;
}
else if (intervalType.SelectedIndex == 2)
{
// hours
totalSeconds = (int )saveInterval.Value * 60 * 60;
}
// set the end time to be totalSeconds from now
endTime = DateTime.Now.AddSeconds(totalSeconds);
}
private void saveSnapshot ()
{
// This function simply saves the image as a PNG file, however
// it can easily be adjusted to save as different image types.
// get webcam image using default 500x500 image size
Image img = webcam1.CaptureImage ();
if (img != null )
{
// Save the image as temp image into the root path. This
// temp image can be displayed on a website or something.
string tempPath = textBoxSavePath.Text + "webcam.png" ;
img.Save (tempPath);
// Also save the image into archive subfolder within the root path.
// build archive subfolder path
string tempPath2 = textBoxSavePath.Text + "archive\\" +
DateTime .Now.ToString ("yyyy" ) + "\\" +
DateTime .Now.ToString ("MM" ) + "\\" +
DateTime .Now.ToString ("dd" ) + "\\" ;
// if archive subfolder path does not exist then create it
if (System.IO.Directory .Exists(tempPath2) == false )
{
System.IO.Directory .CreateDirectory (tempPath2);
}
// build complete archive save path
tempPath2 = tempPath2 + DateTime .Now.ToString ("HH-mm-ss-ff" ) + ".png" ;
// save at complete archive save path
img.Save(tempPath2, System.Drawing.Imaging.ImageFormat .Png);
}
}
private void timer1_Tick (object sender, EventArgs e)
{
// display countdown
TimeSpan tempTS = endTime.Subtract (DateTime .Now);
labelCountdown.Text = "Countdown Time Remaining : " + tempTS.ToString ();
// check if countdown is up
if (tempTS.TotalSeconds <= 0)
{
// take snapshot
saveSnapshot ();
// update snapshot counter
totalSnapshots++;
toolStripStatusLabel1.Text = "Total Snapshots: " + Convert .ToString (totalSnapshots);
// reset countdown
resetCountDown ();
}
}
}
}
When the above code is compiled and executed, the program resembles something similar to the following:
After spinning the bay window plant 180 degrees, I ran this program continuously for a few days and captured the plants leaves move at 30 minute time lapse intervals. I combined the generated snapshot images to produce the following animated results (please pardon the cats outside the window, they just saying hi) :
This WebCam Monitor program is not just limited to Logitech WebCam's, as I have tested that it works well with various other USB WebCam types and Built-In WebCam's as well.
This basic WebCam Monitor program can also be modified and enhanced to support multiple WebCam's at once. If you run this program now as is and you have multiple WebCam's attached to your computer, then you might be prompted to select which WebCam you want to use this program with:
This WebCam Monitor program can also be modified and enhanced to provide and offer other features as well, such as Motion Detection, Microphone, Alert Notifications, People/Face Recognition, etc...
Thank you for reading, I hope you found this blog post (tutorial) educational and helpful.
About | Contact Us | Privacy | Terms & Conditions | © 2024 - T&J Divisions, LLC, All Rights Reserved |