DPI Scaling & Screen Coordinates: Why Your Clicks Land in the Wrong Spot

Your display settings — auto-detected:

Physical Resolution
Logical Resolution
DPR (Scale)
Coordinate Offset

You wrote a script that clicks (960, 540) — worked fine on your old 1080p monitor. Then you upgraded to 4K, Windows set scaling to 150%, and suddenly the same click lands 480 pixels off-target. Your code did not change. The coordinate system underneath it did.

DPI scaling is the most common reason screen coordinates "break" after a monitor upgrade, a new laptop, or dragging a window between displays that scale differently. Below we walk through what actually happens at 100%, 125%, 150%, and 200% — and how to fix it in your scripts.

Open Free Screen Coordinates Tool

Your Current DPI Scaling — Live

Your browser reports the following about your display right now:

Device Pixel Ratio
Reported Screen Size
Estimated Physical Pixels

If the Device Pixel Ratio shows 1.0, you are at 100% scaling — logical and physical pixels are the same. If it shows 1.5, you are at 150% scaling and there is a hidden coordinate transformation happening. Use our screen coordinates tool to see how your logical and physical positions differ in real time.

What Is DPI Scaling (and Why Does It Exist)?

A 27-inch 4K monitor packs 3840×2160 pixels into roughly the same physical space as a 1080p screen. Without scaling, text and icons are tiny — about 140 PPI compared to ~92 PPI on a 24-inch 1080p. Usable, but a strain on the eyes.

So Windows applies DPI scaling. Set it to 150% on that 4K monitor and Windows pretends the screen is only 2560×1440. It draws everything at that smaller size, then stretches it to fill the full 3840×2160. The result: UI ends up a comfortable size, but rendered sharper because each "logical" pixel maps to 1.5 × 1.5 = 2.25 physical pixels.

The problem: Most software (and most automation tools) only see the logical coordinate space. They think the screen is 2560×1440 when it is really 3840×2160. This causes a mismatch between where you think you are clicking and where the click actually registers.

For a deeper explanation of what "physical" and "logical" pixels actually are and how they relate to CSS pixels, device pixels, and device-independent pixels, see our Physical vs Logical Pixels guide.

Logical vs Physical Resolution at Every Scale Factor

Lookup table — find your physical resolution on the left, then read across to see what Windows actually tells applications at each scale factor:

Physical Resolution100% (Logical)125% (Logical)150% (Logical)200% (Logical)
1920×10801920×10801536×8641280×720960×540
2560×14402560×14402048×11521707×9601280×720
3440×14403440×14402752×11522293×9601720×720
3840×21603840×21603072×17282560×14401920×1080

How to read this: If you have a 4K monitor (3840×2160) at 150% scaling, non-DPI-aware applications see a 2560×1440 screen. When such an application asks "what is the screen width?", it gets 2560 — not 3840. When it clicks coordinate (1280, 720), Windows translates that to physical pixel (1920, 1080) before sending it to the display.

This is why your screen resolution comparison scripts may report unexpected values — they are seeing the logical resolution, not the physical one.

The Coordinate Offset Problem: Where Your Click Actually Lands

Let us make this concrete. You want to click the center of your 4K screen — that is physical pixel (1920, 1080). But your automation tool does not know about DPI scaling. It calculates the center of what it sees — the logical 2560×1440 space — and clicks (1280, 720). Windows then scales that click up:

Physical click = logical click × scale factor
Physical click = (1280, 720) × 1.5
Physical click = (1920, 1080)  ← correct by coincidence!

In this case it works out — and it is not pure luck. Windows DPI virtualization scales coordinates uniformly, so any position calculated as a proportion of the screen (center = 50%, corners = 0% or 100%) maps correctly regardless of scaling. The disaster hits when you target an absolute position — a button, an icon, a specific pixel — that is not a simple ratio of the screen size:

You Want to Click (Physical)Your Script Sends (Logical @ 150%)OS Scales To (Physical)Result
(500, 300)(500, 300)(750, 450)❌ 250px off
(960, 540)(960, 540)(1440, 810)❌ 480px off
(1920, 1080)(1280, 720)(1920, 1080)✅ Correct

The problem is clear: if your script uses coordinates from one coordinate system but the target application uses another, every click is offset by the scale factor. The offset gets worse the farther you are from (0, 0).

Real-world scenario: You use PyAutoGUI to click a button at logical position (500, 300) on a 150%-scaled screen. Your script is DPI-unaware, so pyautogui.position() returns (500, 300). But the button is actually at physical position (750, 450). Your click lands at (750, 450) — which is wrong if the button lives in a DPI-aware application like Chrome that positions itself using physical coordinates.

The Fix: SetProcessDPIAware and DPI Awareness

The fix is one API call — tell Windows your app understands high-DPI and wants physical pixels:

// C / C++ (Windows API)
SetProcessDPIAware();

// C# (.NET)
[DllImport("user32.dll")]
static extern bool SetProcessDPIAware();
SetProcessDPIAware();

// Python (ctypes)
import ctypes
ctypes.windll.user32.SetProcessDPIAware()

// Python (recommended for newer Windows)
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(2)  // PROCESS_PER_MONITOR_DPI_AWARE

// Python (recommended for Windows 10 1703+ and Windows 11)
import ctypes
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ctypes.c_void_p(-4)
ctypes.windll.user32.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)

After calling this, coordinate functions return physical pixel values instead of logical values. pyautogui.size() will return 3840×2160 instead of 2560×1440. pyautogui.position() will return real pixel positions. And your clicks will land exactly where you intend.

Which Call Should You Use?

APIBehaviorBest For
SetProcessDPIAware()System-level DPI awareness. Gets the physical resolution of the primary monitor.Single-monitor setups with one scale factor.
SetProcessDpiAwareness(2)Per-monitor DPI awareness. Gets the real physical resolution of each monitor.Multi-monitor setups with mixed DPI (Windows 8.1+).
SetProcessDpiAwarenessContext(-4)Per-monitor DPI awareness V2. Same as above, plus fixes title bar and non-client area scaling bugs on mixed-DPI setups.Recommended for Windows 10 1703+ and Windows 11. See our Multi-Monitor Coordinates guide.

Application Manifest (No Code Required)

If you are building a Windows application, you can declare DPI awareness in your manifest file instead of calling the API:

<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <asmv3:application>
    <asmv3:windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

For the complete PyAutoGUI-specific fix with multi-monitor support and screenshot region offsets, see our PyAutoGUI Coordinates guide.

Mixed-DPI Multi-Monitor: When Things Get Really Messy

Two monitors, different scale factors — say a 4K laptop at 150% and an external 1080p at 100%. The coordinate transformation changes as your cursor moves between screens:

This setup is a pain for automation. But the fix is always the same: call SetProcessDpiAwareness(2) for per-monitor DPI awareness, and you get the real physical coordinates for whichever monitor your cursor is on. The full coordinate math for mixed-DPI setups is in our Multi-Monitor Coordinates guide.

Frequently Asked Questions

Does macOS have the same DPI scaling problem?

Sort of, but macOS handles it more gracefully. Retina displays have a base scale factor of 2×, and apps receive coordinates in "points" (logical pixels). However, users can choose display modes like "More Space" or "Larger Text" in System Settings, which result in effective scale factors like 1.5× or 2.4× — your browser's devicePixelRatio will reflect this. Most Mac automation tools — AppleScript, Hammerspoon — handle the conversion automatically, so you run into the offset problem far less often on macOS.

What about Linux?

Linux display servers (X11 and Wayland) handle DPI scaling differently depending on the compositor and desktop environment. On X11, scaling is often handled per-application using Xft.dpi or environment variables. On Wayland, the compositor handles scaling. Coordinate behavior varies wildly — there is no universal equivalent to SetProcessDPIAware(). Test your scripts on your specific Linux setup.

Why does PyAutoGUI return the wrong screen size?

pyautogui.size() returns the logical resolution by default on Windows. On a 4K display at 150% scaling, it returns 2560×1440 instead of 3840×2160. Call ctypes.windll.user32.SetProcessDPIAware() before importing PyAutoGUI, and it will return the true physical resolution. See our PyAutoGUI Coordinates guide for the full setup.

How do I check my current Windows scaling factor?

Right-click your desktop → Display settings → look for Scale & layout. The percentage shown (100%, 125%, 150%, 200%) is your scale factor. Divide by 100 to get the multiplier. Or use our tool above — it detects your DPR automatically.

Can I just set scaling to 100% and avoid all this?

Sure, but on a 4K monitor everything will be tiny. A 27-inch 4K display at 100% gives you about 140 PPI — readable but not pleasant for extended use. You fix one problem (coordinates) and create another (eye strain). Making your scripts DPI-aware is the better call.

Open Live Screen Coordinate Tracker PyAutoGUI Coordinate Fix