基于C#开发的久坐提醒工具

基于C#开发的久坐提醒工具


一、演示效果

二、关键代码

c 复制代码
using System.Diagnostics;
using System.Drawing.Drawing2D;
using System.Drawing.Text;

namespace MoveReminder;

internal enum ReminderOutcome
{
    /// <summary>用户点了弹窗(确认知道了)。</summary>
    Dismissed,

    /// <summary>用户点了「稍后再提醒」。</summary>
    Snoozed,

    /// <summary>无人理会,倒计时结束自动消失。</summary>
    TimedOut,
}

/// <summary>
/// 桌面右下角弹出的提醒卡片:圆角、滑入淡入动画、
/// 顶部是在线加载的精美图片,下面是鼓励标语,底部有倒计时进度条。
/// 不抢焦点(WS_EX_NOACTIVATE),正在打字时也不会被打断。
/// </summary>
internal sealed class ReminderForm : Form
{
    // 逻辑像素(96 DPI)下的布局常量
    private const int CardW = 400;
    private const int CardH = 340;
    private const int ImageH = 172;
    private const float CornerRadius = 14f;

    private readonly AppConfig _cfg;
    private readonly string _title;
    private readonly string _message;
    private readonly float _scale;
    private readonly CancellationTokenSource _cts = new();
    private readonly Stopwatch _clock = Stopwatch.StartNew();

    private readonly System.Windows.Forms.Timer _timer;
    private readonly Font _titleFont;
    private readonly Font _bodyFont;
    private readonly Font _smallFont;

    private Image? _photo;
    private bool _loadingPhoto;

    private RectangleF _rectPrimary;
    private RectangleF _rectSecondary;
    private RectangleF _rectClose;
    private bool _hoverCard;
    private bool _hoverPrimary;
    private bool _hoverSecondary;
    private bool _hoverClose;

    private TimeSpan _lastFrame;
    private double _pausedMs;
    private double _animT;
    private bool _closing;
    private bool _disposed;

    private Point _targetLocation;
    private Point _entryLocation;
    private Point _exitLocation;

    public ReminderOutcome Outcome { get; private set; } = ReminderOutcome.TimedOut;

    public ReminderForm(AppConfig cfg)
    {
        _cfg = cfg;

        SetStyle(
            ControlStyles.AllPaintingInWmPaint
            | ControlStyles.OptimizedDoubleBuffer
            | ControlStyles.UserPaint
            | ControlStyles.ResizeRedraw,
            true);
        DoubleBuffered = true;

        FormBorderStyle = FormBorderStyle.None;
        StartPosition = FormStartPosition.Manual;
        ShowInTaskbar = false;
        TopMost = true;
        MinimizeBox = false;
        MaximizeBox = false;
        KeyPreview = false;
        Text = "久坐提醒";
        Cursor = Cursors.Hand;

        _scale = DeviceDpi / 96f;
        Width = (int)Math.Round(CardW * _scale);
        Height = (int)Math.Round(CardH * _scale);

        _titleFont = new Font("Microsoft YaHei UI", 23f * _scale, FontStyle.Bold, GraphicsUnit.Pixel);
        _bodyFont = new Font("Microsoft YaHei UI", 14.5f * _scale, FontStyle.Regular, GraphicsUnit.Pixel);
        _smallFont = new Font("Microsoft YaHei UI", 12f * _scale, FontStyle.Regular, GraphicsUnit.Pixel);

        _title = Quotes.NextTitle();
        _message = PickFittingMessage();

        LayoutButtons();

        using (var path = AppIcons.RoundedRect(new RectangleF(0, 0, Width, Height), CornerRadius * _scale))
        {
            Region = new Region(path);
        }

        // 关键:在窗口句柄创建之前就把位置和透明度定好。
        // 否则窗口会先在屏幕 (0,0) 闪一下,再跳到右下角。
        ComputeLocations();
        Location = _entryLocation;
        Opacity = 0.0;

        _timer = new System.Windows.Forms.Timer { Interval = 20 };
        _timer.Tick += OnFrame;
    }

    /// <summary>挑一条能放进两行内的标语,太长就换下一条。</summary>
    private string PickFittingMessage()
    {
        float maxWidth = (CardW - 44) * _scale;
        string best = Quotes.NextEncouragement();

        // 用屏幕 DC 测量,避免在构造函数里提前创建窗口句柄。
        using var g = Graphics.FromHwnd(IntPtr.Zero);

        for (int i = 0; i < 8; i++)
        {
            string candidate = Quotes.NextEncouragement();
            var size = g.MeasureString(candidate, _bodyFont, (int)maxWidth);
            if (size.Width <= maxWidth + 1 && size.Height <= 44 * _scale)
            {
                return candidate;
            }

            if (best.Length > candidate.Length)
            {
                best = candidate;
            }
        }

        return best;
    }

    private float S => _scale;

    /// <summary>设置 MOVEREMINDER_DEBUG=1 时,把弹窗行为写进 %TEMP%\MoveReminder-debug.log。</summary>
    private static void DebugLog(string message)
    {
        if (!string.Equals(Environment.GetEnvironmentVariable("MOVEREMINDER_DEBUG"), "1", StringComparison.Ordinal))
        {
            return;
        }

        try
        {
            File.AppendAllText(
                Path.Combine(Path.GetTempPath(), "MoveReminder-debug.log"),
                $"[{DateTime.Now:HH:mm:ss.fff}] {message}\r\n");
        }
        catch
        {
            // 忽略。
        }
    }

    private void LayoutButtons()
    {
        float btnH = 34 * S;
        float btnY = 290 * S;

        _rectPrimary = new RectangleF(284 * S, btnY, 94 * S, btnH);
        _rectSecondary = new RectangleF(146 * S, btnY, 128 * S, btnH);
        _rectClose = new RectangleF(Width - 38 * S, 12 * S, 26 * S, 26 * S);
    }

    protected override bool ShowWithoutActivation => true;

    protected override CreateParams CreateParams
    {
        get
        {
            var cp = base.CreateParams;
            cp.ExStyle |= 0x08000000; // WS_EX_NOACTIVATE:不抢焦点
            cp.ExStyle |= 0x00000080; // WS_EX_TOOLWINDOW:不出现在 Alt+Tab
            return cp;
        }
    }

    protected override void OnShown(EventArgs e)
    {
        base.OnShown(e);

        Location = _entryLocation;
        Opacity = 0.0;
        TopMost = true; // 再确认一次置顶,避免被全屏或其它置顶窗口盖住

        DebugLog($"shown size={Width}x{Height} entry={_entryLocation} target={_targetLocation} exit={_exitLocation}");

        _lastFrame = _clock.Elapsed;
        _timer.Start();

        if (_cfg.PlaySound)
        {
            try
            {
                System.Media.SystemSounds.Asterisk.Play();
            }
            catch
            {
                // 静音环境无所谓。
            }
        }

        _ = LoadPhotoAsync();
    }

    /// <summary>
    /// 计算右下角的目标位置,以及进出场动画的起止点。
    /// 滑动距离刻意做得很短(几十像素),这样在多显示器下动画不会跨到隔壁屏幕上去。
    /// </summary>
    private void ComputeLocations()
    {
        // 跟随鼠标所在的那块屏幕,多显示器下也能出现在正确的右下角。
        var screen = Screen.FromPoint(Cursor.Position);
        var work = screen.WorkingArea;

        int margin = (int)Math.Round(16 * S);
        _targetLocation = new Point(work.Right - Width - margin, work.Bottom - Height - margin);

        int slideX = (int)Math.Round(38 * S);
        int slideY = (int)Math.Round(18 * S);
        _entryLocation = new Point(_targetLocation.X + slideX, _targetLocation.Y + slideY);
        _exitLocation = new Point(_targetLocation.X + slideX, _targetLocation.Y);
    }

    private async Task LoadPhotoAsync()
    {
        if (!_cfg.LoadOnlineImage)
        {
            return;
        }

        _loadingPhoto = true;

        try
        {
            var image = await ImageFetcher.FetchAsync(_cfg, _cts.Token).ConfigureAwait(true);
            if (IsDisposed || !IsHandleCreated)
            {
                image?.Dispose();
                return;
            }

            if (image is not null)
            {
                _photo?.Dispose();
                _photo = image;
                Invalidate();
            }
        }
        catch
        {
            // 加载失败就用占位渐变图。
        }
        finally
        {
            _loadingPhoto = false;
            if (!IsDisposed && IsHandleCreated)
            {
                Invalidate();
            }
        }
    }

    private void OnFrame(object? sender, EventArgs e)
    {
        if (IsDisposed)
        {
            return;
        }

        var now = _clock.Elapsed;
        double dt = Math.Max(0, (now - _lastFrame).TotalSeconds);
        _lastFrame = now;

        if (_closing)
        {
            AdvanceExitAnimation(dt);
            return;
        }

        AdvanceEntryAnimation(dt);
        UpdateCountdown(dt);

        if (_photo is null && _loadingPhoto)
        {
            Invalidate();
        }
    }

    private void AdvanceEntryAnimation(double dt)
    {
        if (_animT >= 1.0)
        {
            return;
        }

        _animT = Math.Min(1.0, _animT + dt / 0.30);
        double eased = 1 - Math.Pow(1 - _animT, 3); // easeOutCubic

        Location = new Point(
            _entryLocation.X + (int)Math.Round((_targetLocation.X - _entryLocation.X) * eased),
            _entryLocation.Y + (int)Math.Round((_targetLocation.Y - _entryLocation.Y) * eased));

        Opacity = Math.Min(1.0, eased * 1.25);
    }

    private void AdvanceExitAnimation(double dt)
    {
        _animT = Math.Min(1.0, _animT + dt / 0.20);
        double eased = _animT * _animT; // easeInQuad

        Location = new Point(
            _targetLocation.X + (int)Math.Round((_exitLocation.X - _targetLocation.X) * eased),
            _targetLocation.Y);

        Opacity = Math.Max(0.0, 1.0 - eased * 1.4);

        if (_animT >= 1.0)
        {
            DebugLog($"exit animation finished, closing (outcome={Outcome})");
            _timer.Stop();
            Close();
        }
    }

    private void UpdateCountdown(double dt)
    {
        if (_animT < 1.0)
        {
            Invalidate();
            return;
        }

        double elapsed = _clock.Elapsed.TotalSeconds;

        // 鼠标停在卡片上时暂停倒计时,但总时长最多放宽到 3 倍,避免一直赖着不走。
        if (_hoverCard)
        {
            _pausedMs += dt * 1000.0;
        }

        double remaining = _cfg.AutoCloseSeconds - (elapsed - _pausedMs / 1000.0);

        if (remaining <= 0 || elapsed > _cfg.AutoCloseSeconds * 3.0)
        {
            BeginClose(ReminderOutcome.TimedOut);
            return;
        }

        Invalidate();
    }

    private void BeginClose(ReminderOutcome outcome)
    {
        if (_closing)
        {
            return;
        }

        DebugLog($"BeginClose({outcome}) animT was {_animT:0.00}");
        Outcome = outcome;
        _closing = true;
        _animT = 0;
        _lastFrame = _clock.Elapsed;
        _hoverCard = false;
    }

    private void RequestSnooze() => BeginClose(ReminderOutcome.Snoozed);

    // ---------- 绘制 ----------

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        // 全部在 OnPaint 里自绘,避免闪烁。
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        var g = e.Graphics;
        g.SmoothingMode = SmoothingMode.AntiAlias;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.PixelOffsetMode = PixelOffsetMode.HighQuality;
        g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;

        var card = new RectangleF(0, 0, Width, Height);
        using (var cardPath = AppIcons.RoundedRect(card, CornerRadius * S))
        using (var bg = new SolidBrush(Color.White))
        {
            g.FillPath(bg, cardPath);
        }

        DrawPhoto(g);

        // 图片与内容区之间的分隔
        using (var sep = new Pen(Color.FromArgb(235, 238, 242), Math.Max(1f, S)))
        {
            g.DrawLine(sep, 0, ImageH * S, Width, ImageH * S);
        }

        DrawTexts(g);
        DrawButtons(g);
        DrawProgress(g);

        // 一圈很淡的描边,让卡片边缘更利落
        using (var cardPath = AppIcons.RoundedRect(
                   new RectangleF(0.5f * S, 0.5f * S, Width - S, Height - S),
                   CornerRadius * S))
        using (var border = new Pen(Color.FromArgb(30, 15, 23, 42), Math.Max(1f, S)))
        {
            g.DrawPath(border, cardPath);
        }
    }

    private void DrawPhoto(Graphics g)
    {
        var area = new RectangleF(0, 0, Width, ImageH * S);
        var clip = new Rectangle(0, 0, Width, (int)Math.Round(ImageH * S));

        var oldClip = g.Clip;
        g.SetClip(clip);

        if (_photo is not null)
        {
            DrawCover(g, _photo, area);
        }
        else
        {
            DrawPlaceholder(g, area);
        }

        g.Clip = oldClip;

        // 图片底部压一层柔和渐变,让画面与白色内容区衔接自然
        using var fade = new LinearGradientBrush(
            new RectangleF(0, clip.Bottom - 34 * S, Width, 34 * S),
            Color.FromArgb(0, 0, 0, 0),
            Color.FromArgb(38, 0, 0, 0),
            LinearGradientMode.Vertical);
        g.FillRectangle(fade, new RectangleF(0, clip.Bottom - 34 * S, Width, 34 * S));

        DrawCloseButton(g);
    }

    private static void DrawCover(Graphics g, Image image, RectangleF dest)
    {
        float scale = Math.Max(dest.Width / image.Width, dest.Height / image.Height);
        float w = image.Width * scale;
        float h = image.Height * scale;
        float x = dest.X + (dest.Width - w) / 2f;
        float y = dest.Y + (dest.Height - h) / 2f;
        g.DrawImage(image, new RectangleF(x, y, w, h));
    }

    private void DrawPlaceholder(Graphics g, RectangleF area)
    {
        // 每次弹窗随机一组好看的撞色渐变
        (Color a, Color b)[] palettes =
        {
            (Color.FromArgb(99, 102, 241), Color.FromArgb(139, 92, 246)),
            (Color.FromArgb(14, 165, 233), Color.FromArgb(34, 211, 238)),
            (Color.FromArgb(16, 185, 129), Color.FromArgb(20, 184, 166)),
            (Color.FromArgb(245, 158, 11), Color.FromArgb(239, 68, 68)),
            (Color.FromArgb(236, 72, 153), Color.FromArgb(168, 85, 247)),
        };

        var (c1, c2) = palettes[Math.Abs(GetHashCode()) % palettes.Length];

        using (var brush = new LinearGradientBrush(area, c1, c2, 35f))
        {
            g.FillRectangle(brush, area);
        }

        float cx = area.Width / 2f;
        float cy = area.Height / 2f;
        float radius = 17 * S;

        if (_loadingPhoto)
        {
            using var ring = new Pen(Color.FromArgb(70, 255, 255, 255), 3.2f * S)
            {
                StartCap = LineCap.Round,
                EndCap = LineCap.Round,
            };
            float angle = (float)(_clock.Elapsed.TotalMilliseconds / 3.0 % 360.0);
            g.DrawArc(ring, cx - radius, cy - radius - 8 * S, radius * 2, radius * 2, angle, 280);

            using var font = new Font("Microsoft YaHei UI", 13f * S, FontStyle.Regular, GraphicsUnit.Pixel);
            using var fmt = new StringFormat
            {
                Alignment = StringAlignment.Center,
                LineAlignment = StringAlignment.Center,
            };
            using var text = new SolidBrush(Color.FromArgb(225, 255, 255, 255));
            g.DrawString("正在加载精美图片...", font, text, new RectangleF(0, cy + 22 * S, area.Width, 26 * S), fmt);
        }
        else
        {
            using var font = new Font("Microsoft YaHei UI", 34f * S, FontStyle.Bold, GraphicsUnit.Pixel);
            using var fmt = new StringFormat
            {
                Alignment = StringAlignment.Center,
                LineAlignment = StringAlignment.Center,
            };
            using var text = new SolidBrush(Color.FromArgb(235, 255, 255, 255));
            g.DrawString("起 身 动 一 动", font, text, area, fmt);
        }
    }

    private void DrawTexts(Graphics g)
    {
        using var titleBrush = new SolidBrush(Color.FromArgb(17, 24, 39));
        using var bodyBrush = new SolidBrush(Color.FromArgb(75, 85, 99));
        using var tipBrush = new SolidBrush(Color.FromArgb(107, 114, 128));

        using (var fmt = new StringFormat
               {
                   Alignment = StringAlignment.Near,
                   LineAlignment = StringAlignment.Center,
                   Trimming = StringTrimming.EllipsisCharacter,
                   FormatFlags = StringFormatFlags.NoWrap,
               })
        {
            g.DrawString(_title, _titleFont, titleBrush, new RectangleF(22 * S, 184 * S, (CardW - 44) * S, 32 * S), fmt);
        }

        using (var fmt = new StringFormat
               {
                   Alignment = StringAlignment.Near,
                   LineAlignment = StringAlignment.Near,
               })
        {
            g.DrawString(_message, _bodyFont, bodyBrush, new RectangleF(22 * S, 220 * S, (CardW - 44) * S, 46 * S), fmt);
        }

        // 倒计时剩余秒数
        double remaining = Math.Max(0, _cfg.AutoCloseSeconds - (_clock.Elapsed.TotalSeconds - _pausedMs / 1000.0));
        string hint = _hoverCard ? $"已暂停 · {remaining:0} 秒" : $"{remaining:0} 秒后自动关闭";

        using (var fmt = new StringFormat
               {
                   Alignment = StringAlignment.Near,
                   LineAlignment = StringAlignment.Center,
                   FormatFlags = StringFormatFlags.NoWrap,
               })
        {
            g.DrawString(hint, _smallFont, tipBrush, new RectangleF(22 * S, 290 * S, 118 * S, 34 * S), fmt);
        }
    }

    private void DrawButtons(Graphics g)
    {
        // 主按钮
        using (var path = AppIcons.RoundedRect(_rectPrimary, 9 * S))
        using (var brush = new SolidBrush(_hoverPrimary
                   ? Color.FromArgb(5, 150, 105)
                   : Color.FromArgb(16, 185, 129)))
        {
            g.FillPath(brush, path);
        }

        DrawCenteredText(g, "这就去 ✓", _rectPrimary, Color.White, _bodyFont);

        // 次按钮
        if (_hoverSecondary)
        {
            using var path = AppIcons.RoundedRect(_rectSecondary, 9 * S);
            using var brush = new SolidBrush(Color.FromArgb(243, 244, 246));
            g.FillPath(brush, path);
        }

        using (var path = AppIcons.RoundedRect(
                   new RectangleF(_rectSecondary.X + 0.5f * S, _rectSecondary.Y + 0.5f * S,
                       _rectSecondary.Width - S, _rectSecondary.Height - S), 9 * S))
        using (var pen = new Pen(Color.FromArgb(209, 213, 219), Math.Max(1f, S)))
        {
            g.DrawPath(pen, path);
        }

        DrawCenteredText(g, $"{_cfg.SnoozeMinutes} 分钟后再提醒", _rectSecondary,
            Color.FromArgb(55, 65, 81), _smallFont);
    }

    private void DrawCloseButton(Graphics g)
    {
        using var brush = new SolidBrush(_hoverClose
            ? Color.FromArgb(190, 0, 0, 0)
            : Color.FromArgb(110, 0, 0, 0));
        g.FillEllipse(brush, _rectClose);

        using var pen = new Pen(Color.White, 1.7f * S)
        {
            StartCap = LineCap.Round,
            EndCap = LineCap.Round,
        };

        float pad = 8.4f * S;
        g.DrawLine(pen, _rectClose.Left + pad, _rectClose.Top + pad, _rectClose.Right - pad, _rectClose.Bottom - pad);
        g.DrawLine(pen, _rectClose.Right - pad, _rectClose.Top + pad, _rectClose.Left + pad, _rectClose.Bottom - pad);
    }

    private void DrawCenteredText(Graphics g, string text, RectangleF rect, Color color, Font font)
    {
        using var fmt = new StringFormat
        {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center,
            FormatFlags = StringFormatFlags.NoWrap,
        };
        using var brush = new SolidBrush(color);
        g.DrawString(text, font, brush, rect, fmt);
    }

    private void DrawProgress(Graphics g)
    {
        float trackY = Height - 4 * S;
        float trackH = 4 * S;

        using (var track = new SolidBrush(Color.FromArgb(241, 245, 249)))
        {
            g.FillRectangle(track, 0, trackY, Width, trackH);
        }

        double remaining = Math.Max(0, _cfg.AutoCloseSeconds - (_clock.Elapsed.TotalSeconds - _pausedMs / 1000.0));
        double ratio = Math.Clamp(remaining / Math.Max(1, _cfg.AutoCloseSeconds), 0, 1);
        if (ratio <= 0)
        {
            return;
        }

        Color barColor = _hoverCard
            ? Color.FromArgb(148, 163, 184)
            : ratio < 0.25
                ? Color.FromArgb(245, 158, 11)
                : Color.FromArgb(16, 185, 129);

        using var brush = new SolidBrush(barColor);
        g.FillRectangle(brush, 0, trackY, (float)(Width * ratio), trackH);
    }

    // ---------- 交互 ----------

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        bool hp = _rectPrimary.Contains(e.Location);
        bool hs = _rectSecondary.Contains(e.Location);
        bool hc = _rectClose.Contains(e.Location);
        bool hover = !_hoverCard || hp != _hoverPrimary || hs != _hoverSecondary || hc != _hoverClose;

        _hoverCard = true;
        _hoverPrimary = hp;
        _hoverSecondary = hs;
        _hoverClose = hc;

        if (hover)
        {
            Invalidate();
        }
    }

    protected override void OnMouseLeave(EventArgs e)
    {
        base.OnMouseLeave(e);
        _hoverCard = false;
        _hoverPrimary = false;
        _hoverSecondary = false;
        _hoverClose = false;
        Invalidate();
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);

        DebugLog($"mouseup button={e.Button} at={e.Location} closing={_closing} animT={_animT:0.00} " +
                 $"secondary={_rectSecondary.Contains(e.Location)} close={_rectClose.Contains(e.Location)} " +
                 $"primary={_rectPrimary.Contains(e.Location)}");

        if (e.Button == MouseButtons.Right)
        {
            RequestSnooze();
            return;
        }

        if (e.Button != MouseButtons.Left || _closing)
        {
            return;
        }

        if (_rectClose.Contains(e.Location))
        {
            BeginClose(ReminderOutcome.Dismissed);
        }
        else if (_rectSecondary.Contains(e.Location))
        {
            RequestSnooze();
        }
        else
        {
            // 点卡片任意位置都算「我知道了」
            BeginClose(ReminderOutcome.Dismissed);
        }
    }

    protected override void OnDeactivate(EventArgs e)
    {
        base.OnDeactivate(e);
        // 不抢焦点,所以这里不做任何事。
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && !_disposed)
        {
            _disposed = true;

            try
            {
                _timer.Stop();
                _timer.Dispose();
                _cts.Cancel();
                _cts.Dispose();
                _photo?.Dispose();
                _titleFont.Dispose();
                _bodyFont.Dispose();
                _smallFont.Dispose();
            }
            catch
            {
                // 关闭阶段不要因为清理异常影响退出。
            }
        }

        base.Dispose(disposing);
    }
}

三、程序链接

https://download.csdn.net/download/u013083044/93665476

相关推荐
平生幻2 小时前
unbuntu虚拟机确认python安装位置
开发语言·python
运维行者_2 小时前
PHP性能监控怎么做?从响应时间到慢函数的6个关键指标
运维·服务器·开发语言·网络·支持向量机·php·接口隔离原则
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Nacos 配置管理之添加配置与 dataId 命名规范
java·开发语言·spring cloud
阿俊-全栈开发2 小时前
LikeShop单商户Java商城如何从容承接高并发流量?
java·开发语言·spring boot·spring·系统架构
云樱梦海2 小时前
5 分钟上手 IndexTTS 2.5 便携包:不用装 Python、不挑显卡、本地离线跑语音克隆
开发语言·python·tts·indextts2.5
米糕闯编程2 小时前
鱼香ros2(十二)C++话题订阅与发布
开发语言·c++·ros2·话题
脉动数据行情12 小时前
Go 语言实战|对接美股英伟达 NVDA 实时行情 生产级 WebSocket 方案
开发语言·websocket·golang
FLJwu3 小时前
运动相机按键与连接器硬件选型实战02:USB‑C接口选型|防水失效、虚接断连、腐蚀烧口量产大坑
c语言·开发语言·经验分享·数码相机·相机·运动相机·oem 量产
Do_It_Today3 小时前
php xml 入门学习资料
开发语言·php