1

I would like to create a count down timer that displays in the lower right hand of the screen with slightly transparent text that can be seen no matter what you have on the screen. Think of something like the windows not activated watermark message that appears when your windows copy isn't activated. That displays in the lower right overlaying all windows and is "stuck" to the screen.

Is there a way to do this via a program? C# preferred.

In my research so far I only came across Deskbands which allow you to put something in the task bar but not outside of it.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
SolidSnake4444
  • 3,372
  • 6
  • 40
  • 63
  • 1
    Thought experiment: [What if two programs did this?](https://devblogs.microsoft.com/oldnewthing/20050607-00/?p=35413) – IInspectable Apr 20 '22 at 21:41
  • 1
    [Windows Forms: Pass clicks through a partially transparent always-on-top window](https://stackoverflow.com/a/39856049/3110834) – Reza Aghaei Apr 20 '22 at 22:19
  • 1
    An alternative is using a DirectX overlay, e.g., [GameOverlay.Net](https://github.com/michel-pi/GameOverlay.Net) – Jimi Apr 21 '22 at 00:23

1 Answers1

4

Note: The following answer creates a normal semi-transparent topmost form with smooth edge. It's not exactly like windows activation text, for example it goes behind tooltip windows or goes behind menus, but it stays on top of other non-topmost windows.

You can create a Layered Window by setting WS_EX_LAYERED style, then you can assign a smooth-edge transparent region to the window. You can also set WS_EX_TRANSPARENT for the window, then it will ignore the mouse event. Then to make it always on top set its TopMost to true. And finally if you want to make it semi-transparent use suitable Opacity value.

enter image description here

Example

In the following example, I've created an overlay form which is always on top and shows time:

1 - Add the following class which contains required native methods to your project:

using System;
using System.Runtime.InteropServices;
public class NativeMethods
{
    public const int WS_EX_LAYERED = 0x80000;
    public const int HTCAPTION = 0x02;
    public const int WM_NCHITTEST = 0x84;
    public const int ULW_ALPHA = 0x02;
    public const byte AC_SRC_OVER = 0x00;
    public const byte AC_SRC_ALPHA = 0x01;
    public const int WS_EX_TRANSPARENT = 0x20;
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;

        public POINT(int x, int y)
        { this.x = x; this.y = y; }
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct SIZE
    {
        public int cx;
        public int cy;

        public SIZE(int cx, int cy)
        { this.cx = cx; this.cy = cy; }
    }
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct BLENDFUNCTION
    {
        public byte BlendOp;
        public byte BlendFlags;
        public byte SourceConstantAlpha;
        public byte AlphaFormat;
    }
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst,
        ref POINT pptDst, ref SIZE psize, IntPtr hdcSrc, ref POINT pprSrc,
        int crKey, ref BLENDFUNCTION pblend, int dwFlags);
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr GetDC(IntPtr hWnd);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DeleteDC(IntPtr hdc);
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DeleteObject(IntPtr hObject);
}

2 - Add the following class which is a base class for semi-transparent smooth-edge windows forms:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Imaging;
using static NativeMethods;
public class PerPixelAlphaForm : Form
{
    public PerPixelAlphaForm()
    {
        this.FormBorderStyle = FormBorderStyle.None;
        this.ShowInTaskbar = false;
        this.TopMost = true;
    }
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams createParams = base.CreateParams;
            if (!DesignMode)
                createParams.ExStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT;
            return createParams;
        }
    }
    public void SelectBitmap(Bitmap bitmap, int opacity = 255)
    {
        if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
        {
            throw new ApplicationException(
                "The bitmap must be 32bpp with alpha-channel.");
        }
        IntPtr screenDc = GetDC(IntPtr.Zero);
        IntPtr memDc = CreateCompatibleDC(screenDc);
        IntPtr hBitmap = IntPtr.Zero;
        IntPtr hOldBitmap = IntPtr.Zero;
        try
        {
            hBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
            hOldBitmap = SelectObject(memDc, hBitmap);
            SIZE newSize = new SIZE(bitmap.Width, bitmap.Height);
            POINT sourceLocation = new POINT(0, 0);
            POINT newLocation = new POINT(this.Left, this.Top);
            BLENDFUNCTION blend = new BLENDFUNCTION();
            blend.BlendOp = AC_SRC_OVER;
            blend.BlendFlags = 0;
            blend.SourceConstantAlpha = (byte)opacity;
            blend.AlphaFormat = AC_SRC_ALPHA;
            UpdateLayeredWindow(this.Handle, screenDc, ref newLocation, 
                ref newSize, memDc, ref sourceLocation, 0, ref blend, ULW_ALPHA);
        }
        finally
        {
            ReleaseDC(IntPtr.Zero, screenDc);
            if (hBitmap != IntPtr.Zero)
            {
                SelectObject(memDc, hOldBitmap);
                DeleteObject(hBitmap);
            }
            DeleteDC(memDc);
        }
    }
}

3 - Then add the following class which is a form, having a timer that shows time:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Windows.Forms;
public partial class Form1 : PerPixelAlphaForm
{
    private IContainer components = null;
    private Timer timer1;
    private Bitmap image;
    public Form1()
    {
        this.components = new Container();
        this.timer1 = new Timer(this.components);
        this.timer1.Enabled = true;
        this.timer1.Interval = 500;
        this.timer1.Tick += new EventHandler(this.timer1_Tick);
        this.StartPosition = FormStartPosition.Manual;
        this.Location = new Point(300, 300);
        this.Size = new Size(800, 500);
        this.image = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);
    }
    private void timer1_Tick(object sender, EventArgs e)
    {
        using (var g = Graphics.FromImage(image))
        {
            g.Clear(Color.Transparent);
            g.SmoothingMode = SmoothingMode.AntiAlias;
            g.TextRenderingHint = TextRenderingHint.AntiAlias;
            g.DrawString(DateTime.Now.ToString("HH:mm:ss"),
                new Font(this.Font.FontFamily, 60, FontStyle.Bold), Brushes.Black,
                ClientRectangle, StringFormat.GenericDefault);
            SelectBitmap(image, 150);
        }
    }
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
            components.Dispose();
        if (disposing && image != null)
            image.Dispose();
        base.Dispose(disposing);
    }
}

You can find more information in the following posts:

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Rather than using the Opacity property, you can simply clear the Bitmap with a semi-transparent color, e.g., `g.Clear(Color.FromArgb(120, 32, 32, 32));` will give a gray-ish background (not bad with a white text). – Jimi Apr 21 '22 at 00:17
  • @Jimi Yes of course, makes sense. Or even draw text with semi-transparent color over a transparent background. – Reza Aghaei Apr 21 '22 at 00:31
  • @RezaAghaei Thanks for this, so far this seems to be what I'm looking for. I have two questions, Topmost is not working for me so when I click to another window the timer goes in the background. I started a new winforms project with .net 5 so not sure if that is affecting anything. 2nd question is could you explain why the form ends up being a draw image bitmap? Like why do we have to draw an image to get something to display as opposed to filling out a forms page with various controls like buttons and the such? – SolidSnake4444 Apr 24 '22 at 17:08
  • @SolidSnake4444 I tried the solution with .NET 4.8, 5.0, and 6.0 and it works as expected. I'll share a GitHub repo with you later. – Reza Aghaei Apr 24 '22 at 21:16
  • @SolidSnake4444 Applying WS_EX_TRANSPARENT style is to make the form not handle the clicks, then there's no point on having controls on the form, unless for display purpose, for example some labels and etc. Applying WS_EX_LAYERED is to have per pixel alpha and smooth edge for the form with custom shape (just string, without anything around the string). If a normal borderless form with 0.5 opacity is good for you, you don't need this style. Then just look into the first link on bottom of my answer. – Reza Aghaei Apr 24 '22 at 21:20