BlogScreenshots on your device
How to Take Screenshots on a Timer in C#
Capture a monitor or region every few seconds in C# with a Windows Forms timer, save unique PNG files, and handle failures safely.
Use a Windows Forms System.Windows.Forms.Timer to trigger each capture, then copy pixels with Graphics.CopyFromScreen into a Bitmap and save it with a unique filename. The timer interval is measured in milliseconds. Pass a monitor’s bounds for a full display or a custom rectangle for a region.
Microsoft describes the Windows Forms timer as a component that raises an event at regular intervals. Graphics.CopyFromScreen performs a bit-block transfer of color data from the screen to a drawing surface. See the Timer component documentation and CopyFromScreen API reference.
Complete Windows Forms example
Create a Windows Forms project targeting Windows, replace the form code with the following, and run it. It captures the primary monitor every five seconds into a TimedCaptures folder under the user’s Pictures directory.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
public partial class MainForm : Form
{
private readonly Timer captureTimer = new() { Interval = 5000 };
private readonly string outputDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"TimedCaptures");
public MainForm()
{
InitializeComponent();
Directory.CreateDirectory(outputDirectory);
captureTimer.Tick += CaptureTimer_Tick;
captureTimer.Start();
FormClosed += (_, _) => captureTimer.Dispose();
}
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
Rectangle capture = Screen.PrimaryScreen?.Bounds
?? throw new InvalidOperationException("No primary screen is available.");
string fileName = Path.Combine(
outputDirectory,
$"capture-{DateTime.Now:yyyyMMdd-HHmmss-fff}.png");
using var bitmap = new Bitmap(capture.Width, capture.Height);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.CopyFromScreen(capture.Location, Point.Empty, capture.Size);
}
bitmap.Save(fileName, ImageFormat.Png);
}
}
The source point is capture.Location, the destination point is Point.Empty because the bitmap begins at (0, 0), and capture.Size supplies the copied width and height. The using statements release the graphics surface and bitmap after every frame.
How the timed capture works
- Set the interval.
Interval = 5000means the timer raisesTickapproximately every five seconds. - Choose a rectangle. A monitor’s
Boundscaptures the entire display. A customRectanglecaptures only the selected area. - Allocate a bitmap. Its dimensions must match the rectangle, or the result will be cropped or padded.
- Copy pixels.
CopyFromScreencopies from the desktop into the bitmap-backed graphics object. - Write a unique file. Milliseconds in the timestamp prevent most collisions when captures happen close together.
- Dispose resources. Dispose the timer when the form closes and dispose each bitmap and graphics object after saving.
Capture a different monitor
Enumerate Screen.AllScreens and select the display you need. Monitor coordinates can be negative when a display is positioned to the left or above the primary monitor, so use the returned Bounds rather than assuming coordinates start at zero.
private static Rectangle GetMonitorBounds(int index)
{
if (index < 0 || index >= Screen.AllScreens.Length)
throw new ArgumentOutOfRangeException(nameof(index));
return Screen.AllScreens[index].Bounds;
}
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
Rectangle capture = GetMonitorBounds(1); // second display
SaveCapture(capture);
}
private void SaveCapture(Rectangle capture)
{
string fileName = Path.Combine(
outputDirectory,
$"capture-{DateTime.Now:yyyyMMdd-HHmmss-fff}.png");
using var bitmap = new Bitmap(capture.Width, capture.Height);
using var graphics = Graphics.FromImage(bitmap);
graphics.CopyFromScreen(capture.Location, Point.Empty, capture.Size);
bitmap.Save(fileName, ImageFormat.Png);
}
Capture a fixed region
Use desktop coordinates for a region such as a status panel. The rectangle’s X and Y values are the source point; its width and height control the bitmap size.
private static readonly Rectangle Region = new Rectangle(
x: 100,
y: 100,
width: 1200,
height: 700);
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
SaveCapture(Region);
}
Validate the rectangle before allocating a bitmap. A zero or negative width/height is invalid, and a region may extend beyond the available desktop if a window or monitor layout changes.
Stop after a fixed number of frames
private int framesRemaining = 60;
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
if (framesRemaining-- <= 0)
{
captureTimer.Stop();
captureTimer.Dispose();
return;
}
SaveCapture(Screen.PrimaryScreen?.Bounds
?? throw new InvalidOperationException("No primary screen is available."));
}
For a time limit, compare DateTime.UtcNow with a stored end time. Stopping the timer before disposing it prevents additional tick events.
Choose the interval and image format
| Decision | Guidance |
|---|---|
| Interval | Use milliseconds. Longer intervals reduce disk writes and storage; shorter intervals create more files and consume more CPU, memory bandwidth and disk space. |
| PNG | Lossless and suitable for text and interface captures. It can produce large files for full monitors. |
| JPEG | Smaller files when some visual loss is acceptable. Set encoder quality deliberately. |
| Bitmap size | Retina or high-resolution displays require more memory per frame. Dispose each frame promptly. |
Keep the UI responsive
A Windows Forms timer runs on the UI thread. The capture itself should be short, but encoding and disk I/O can delay painting or input when images are large. Keep the Tick handler bounded, or capture into memory and hand completed bitmaps to a producer/consumer queue. The worker that writes files must own and dispose each bitmap exactly once. Do not update controls from that worker; marshal UI updates with BeginInvoke.
Do not allow overlapping captures. A slow save can otherwise cause multiple frames to queue. A simple guard is:
private bool captureInProgress;
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
if (captureInProgress)
return;
captureInProgress = true;
try
{
SaveCapture(Screen.PrimaryScreen?.Bounds
?? throw new InvalidOperationException("No primary screen is available."));
}
finally
{
captureInProgress = false;
}
}
Handle failures and make filenames reliable
CopyFromScreen can raise Win32Exception when Windows cannot perform the operation. A long-running capture service should catch it, log enough context to diagnose the display state, and decide whether to retry or stop.
using System.ComponentModel;
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
try
{
Rectangle capture = Screen.PrimaryScreen?.Bounds
?? throw new InvalidOperationException("No primary screen is available.");
SaveCapture(capture);
}
catch (Win32Exception ex)
{
LogError("Screen copy failed", ex);
// Continue on the next tick, or stop the timer if capture is mandatory.
}
catch (IOException ex)
{
LogError("Capture file could not be written", ex);
}
}
private static void LogError(string message, Exception ex)
{
Console.Error.WriteLine($"{DateTime.UtcNow:o} {message}: {ex}");
}
Timestamp names can still collide if multiple processes write to the same folder. Add a process-specific suffix or use an atomic sequence number when collision-free names are required. Ensure the output directory exists and that the process has write permission.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| No files appear | The timer was never started, or the form closed. | Call captureTimer.Start() after subscribing to Tick; keep the form alive. |
| Only part of a monitor is captured | The rectangle is smaller than the monitor bounds. | Use Screen.PrimaryScreen.Bounds or the selected screen’s Bounds. |
| Black or incomplete image | A protected, minimized, secure or hardware-rendered surface may not be available through this desktop copy operation. | Capture a visible desktop region and handle the failure; this API is not a replacement for application-specific rendering capture. |
Win32Exception |
Windows rejected the screen-copy operation or the display changed. | Catch it, log it, re-read monitor bounds, and retry on a later tick or stop cleanly. |
ArgumentException from Bitmap |
Width or height is zero/negative, or exceeds available resources. | Validate the rectangle and reduce capture dimensions. |
| Out-of-memory errors | Bitmaps are retained or captures are too large/frequent. | Dispose every bitmap and graphics object immediately; reduce interval or region size. |
| UI freezes | PNG encoding or disk writes block the UI thread. | Keep capture short and move file writing to a controlled background queue. |
| Overwritten files | Filenames contain only seconds or a fixed name. | Include milliseconds and, for concurrent processes, a unique suffix. |
Operational checklist
- Choose the exact monitor or region before starting.
- Use an interval appropriate for the amount of storage available.
- Create the output directory once and verify write access.
- Use unique filenames.
- Dispose
Graphics,Bitmapand the timer. - Catch
Win32Exceptionand file-system exceptions. - Prevent overlapping captures.
- Rotate or delete old files if the process runs indefinitely.
- Record failures and skipped frames so a missing image is distinguishable from a quiet screen.
Or skip the browser setup
If your goal is scheduled screenshots of web pages rather than the Windows desktop, ScreenshotNeo provides a single HTTP request. It removes cookie and consent banners, newsletter popups and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.
See the ScreenshotNeo API documentation for all options. This is a complete request you can run from a shell:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
ScreenshotNeo supports full-page and element captures, device presets or custom viewports, dark mode, retina scale, waits, custom CSS and JavaScript, headers, cookies, blocking rules, caching, PDFs, async jobs, bulk capture and signed links. It has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can I capture every second?
Yes, set Interval to 1000, but measure CPU, memory, encoding time and disk growth. Skip a tick when the previous capture is still running.
Does this capture all monitors at once?
No. Capture each screen’s Bounds separately, or construct one desktop rectangle if you specifically need a combined image.
Why are coordinates negative?
Windows uses a virtual desktop coordinate system. A monitor placed left of or above the primary display has negative coordinates; use the values returned by Screen.Bounds.
Can a Windows Forms timer run when the application is minimized?
The timer can raise events while the process runs, but minimized, locked or unavailable desktop content may not produce a useful image. Handle failures and validate the captured output for your use case.
How do I capture a web page instead of my desktop?
Use the ScreenshotNeo HTTP request above when you need a rendered URL, page cleanup and API-managed capture behavior rather than pixels from the local Windows desktop.


