Add NightShot source

This commit is contained in:
f1den
2026-07-31 17:03:49 +03:00
parent 0174ecaf32
commit dfcbacfb0c
33 changed files with 8358 additions and 1 deletions
+23
View File
@@ -0,0 +1,23 @@
DEV_README.md
THIRD_PARTY_NOTICES.md
dist/
build/
*.o
*.obj
*.exe
*.res
*.dll
*.pdb
*.ilk
.vscode/
.idea/
*.zip
*.sha256
.DS_Store
Thumbs.db
desktop.ini
+31 -1
View File
@@ -1,3 +1,33 @@
# NightShot
NightShot — быстрый нативный Win64-инструмент для создания скриншотов и записи GIF. Выделение области, аннотации, ColorPicker, загрузка на свой API-сервер, системные уведомления, мультимониторный режим и автосохранение.
NightShot — нативное приложение для Windows 10/11, предназначенное для быстрого создания и редактирования скриншотов, а также записи GIF с выбранной области экрана.
Приложение написано на C++20 с использованием WinAPI и GDI+, не требует CMake, сторонних UI-фреймворков или установки дополнительных runtime-компонентов.
## Возможности
- Захват произвольной области экрана через `PrintScreen`
- Поддержка нескольких мониторов и разных значений DPI
- Перемещение и изменение размера выделенной области
- Карандаш, линии, стрелки, прямоугольники, маркер и текст
- Настройка толщины инструментов колёсиком мыши
- ColorPicker с пипеткой, HEX, RGBA и HSV
- Сохраняемая палитра цветов 8×6
- Копирование изображения в буфер обмена
- Сохранение снимков без перезаписи существующих файлов
- Запись GIF через `Ctrl + PrintScreen`
- Выбор FPS, качества, курсора и лимита записи
- Автоматическое сохранение GIF и помещение результата в буфер
- Загрузка изображений на пользовательский API-сервер
- Системные уведомления Windows
- Иконка в трее и окно настроек
- Автозагрузка и запуск с правами администратора
- Полностью нативная Win64-сборка
## Сборка
Для сборки требуется MinGW-w64 с поддержкой C++20.
```bat
build.bat
```
+139
View File
@@ -0,0 +1,139 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
cd /d "%~dp0"
title NightShot Win64 build
set "EXITCODE=0"
set "PAUSE_ON_EXIT=1"
if /I "%~1"=="--no-pause" set "PAUSE_ON_EXIT=0"
set "CXX_PATH="
set "WINDRES_PATH="
set "RES_OBJECT="
rem Prefer an explicitly named MinGW-w64 compiler, then a 64-bit g++ from PATH.
for /f "delims=" %%P in ('where x86_64-w64-mingw32-g++.exe 2^>nul') do if not defined CXX_PATH set "CXX_PATH=%%P"
if not defined CXX_PATH for /f "delims=" %%P in ('where g++.exe 2^>nul') do if not defined CXX_PATH set "CXX_PATH=%%P"
if not defined CXX_PATH (
echo [ERROR] A Win64 MinGW-w64 g++ compiler was not found.
echo Install MSYS2 UCRT64/MinGW64 or add its bin directory to PATH.
set "EXITCODE=1"
goto :finish
)
for /f "delims=" %%T in ('""!CXX_PATH!" -dumpmachine 2^>nul"') do set "TARGET=%%T"
echo !TARGET! | findstr /I /C:"x86_64-w64-mingw32" >nul
if errorlevel 1 (
echo [ERROR] The detected compiler is not a Win64 MinGW-w64 compiler.
echo Compiler: !CXX_PATH!
echo Target: !TARGET!
set "EXITCODE=1"
goto :finish
)
rem windres is often next to g++.exe but is not always added to PATH.
for %%P in ("!CXX_PATH!") do set "TOOLBIN=%%~dpP"
for %%R in ("!TOOLBIN!x86_64-w64-mingw32-windres.exe" "!TOOLBIN!!TARGET!-windres.exe" "!TOOLBIN!windres.exe") do (
if not defined WINDRES_PATH if exist "%%~fR" set "WINDRES_PATH=%%~fR"
)
if not defined WINDRES_PATH for /f "delims=" %%P in ('where x86_64-w64-mingw32-windres.exe 2^>nul') do if not defined WINDRES_PATH set "WINDRES_PATH=%%P"
if not defined WINDRES_PATH for /f "delims=" %%P in ('where windres.exe 2^>nul') do if not defined WINDRES_PATH set "WINDRES_PATH=%%P"
if not exist build mkdir build
if not exist dist mkdir dist
set "COMMON=-std=c++20 -O2 -fms-extensions -Wall -Wextra -Wpedantic -DUNICODE -D_UNICODE -D_WIN32_WINNT=0x0A00 -D_WIN32_IE=0x0600 -finput-charset=UTF-8 -Iinclude -ffunction-sections -fdata-sections"
echo Compiler: !CXX_PATH!
echo Target: !TARGET!
echo.
echo [1/13] Compiling capture.cpp
"!CXX_PATH!" !COMMON! -c src\capture.cpp -o build\capture.o
if errorlevel 1 goto :build_failed
echo [2/13] Compiling image_io.cpp
"!CXX_PATH!" !COMMON! -c src\image_io.cpp -o build\image_io.o
if errorlevel 1 goto :build_failed
echo [3/13] Compiling gif_recorder.cpp
"!CXX_PATH!" !COMMON! -c src\gif_recorder.cpp -o build\gif_recorder.o
if errorlevel 1 goto :build_failed
echo [4/13] Compiling gif_options_window.cpp
"!CXX_PATH!" !COMMON! -c src\gif_options_window.cpp -o build\gif_options_window.o
if errorlevel 1 goto :build_failed
echo [5/13] Compiling overlay.cpp
"!CXX_PATH!" !COMMON! -c src\overlay.cpp -o build\overlay.o
if errorlevel 1 goto :build_failed
echo [6/13] Compiling notifications.cpp
"!CXX_PATH!" !COMMON! -c src\notifications.cpp -o build\notifications.o
if errorlevel 1 goto :build_failed
echo [7/13] Compiling settings.cpp
"!CXX_PATH!" !COMMON! -c src\settings.cpp -o build\settings.o
if errorlevel 1 goto :build_failed
echo [8/13] Compiling startup.cpp
"!CXX_PATH!" !COMMON! -c src\startup.cpp -o build\startup.o
if errorlevel 1 goto :build_failed
echo [9/13] Compiling settings_window.cpp
"!CXX_PATH!" !COMMON! -c src\settings_window.cpp -o build\settings_window.o
if errorlevel 1 goto :build_failed
echo [10/13] Compiling uploader.cpp
"!CXX_PATH!" !COMMON! -c src\uploader.cpp -o build\uploader.o
if errorlevel 1 goto :build_failed
echo [11/13] Compiling main.cpp
"!CXX_PATH!" !COMMON! -c src\main.cpp -o build\main.o
if errorlevel 1 goto :build_failed
if defined WINDRES_PATH (
echo [12/13] Compiling resources
"!WINDRES_PATH!" -Iinclude -Ires res\app.rc -O coff -o build\app_res.o
if errorlevel 1 goto :build_failed
set "RES_OBJECT=build\app_res.o"
) else (
echo [12/13] Resources skipped
echo [WARN] Win64 windres was not found. The EXE will still build, but without the embedded icon/version/manifest.
echo [WARN] Install the full MSYS2 mingw-w64-x86_64-binutils package to restore resources.
)
echo [13/13] Linking Win64 executable
"!CXX_PATH!" -municode -mwindows -static -static-libgcc -static-libstdc++ ^
-Wl,--gc-sections -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va ^
build\capture.o build\image_io.o build\gif_recorder.o build\gif_options_window.o ^
build\overlay.o build\notifications.o ^
build\settings.o build\startup.o build\settings_window.o build\uploader.o ^
build\main.o !RES_OBJECT! ^
-lgdiplus -lgdi32 -luser32 -lshell32 -lshcore -lole32 -lruntimeobject ^
-lcomdlg32 -luuid -lwinhttp -ladvapi32 -lwindowscodecs ^
-o dist\NightShot.exe
if errorlevel 1 goto :build_failed
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$b=[IO.File]::ReadAllBytes('dist\NightShot.exe');$pe=[BitConverter]::ToInt32($b,0x3c);$magic=[BitConverter]::ToUInt16($b,$pe+24);if($magic-ne 0x20b){Write-Error 'Output is not PE32+ Win64';exit 1}"
if errorlevel 1 goto :build_failed
echo.
echo [OK] Built: dist\NightShot.exe [Win64 / PE32+]
set "EXITCODE=0"
goto :finish
:build_failed
set "EXITCODE=!ERRORLEVEL!"
if "!EXITCODE!"=="0" set "EXITCODE=1"
echo.
echo [ERROR] Build failed with exit code !EXITCODE!.
:finish
echo.
if "!PAUSE_ON_EXIT!"=="1" (
echo Press any key to close this window...
pause >nul
)
exit /b !EXITCODE!
+5
View File
@@ -0,0 +1,5 @@
@echo off
cd /d "%~dp0"
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
echo Cleaned.
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <windows.h>
inline constexpr UINT WM_NIGHTSHOT_SCREENSHOT_COPIED = WM_APP + 12;
inline constexpr UINT WM_NIGHTSHOT_OPEN_SETTINGS = WM_APP + 13;
inline constexpr UINT WM_NIGHTSHOT_SETTINGS_CHANGED = WM_APP + 14;
inline constexpr UINT WM_NIGHTSHOT_UPLOAD_FINISHED = WM_APP + 15;
inline constexpr UINT WM_NIGHTSHOT_BEGIN_GIF = WM_APP + 16;
inline constexpr UINT WM_NIGHTSHOT_GIF_FINISHED = WM_APP + 17;
inline constexpr UINT WM_NIGHTSHOT_SCREENSHOT_SAVED = WM_APP + 18;
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "config.hpp"
#include <windows.h>
struct ScreenCapture {
HDC memoryDc = nullptr;
HBITMAP bitmap = nullptr;
HGDIOBJ oldBitmap = nullptr;
void* pixels = nullptr;
int x = 0;
int y = 0;
int width = 0;
int height = 0;
int stride = 0;
ScreenCapture() = default;
ScreenCapture(const ScreenCapture&) = delete;
ScreenCapture& operator=(const ScreenCapture&) = delete;
ScreenCapture(ScreenCapture&& other) noexcept;
ScreenCapture& operator=(ScreenCapture&& other) noexcept;
~ScreenCapture();
[[nodiscard]] bool valid() const noexcept { return memoryDc && bitmap && width > 0 && height > 0; }
void reset() noexcept;
};
[[nodiscard]] ScreenCapture captureVirtualDesktop();
+119
View File
@@ -0,0 +1,119 @@
#pragma once
#include "config.hpp"
#include <windows.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cwctype>
#include <iomanip>
#include <sstream>
#include <string>
namespace color_picker_math {
struct Hsv {
float h = 0.0f; // [0, 360)
float s = 0.0f; // [0, 1]
float v = 0.0f; // [0, 1]
};
inline Hsv rgbToHsv(BYTE red, BYTE green, BYTE blue) {
const float r = static_cast<float>(red) / 255.0f;
const float g = static_cast<float>(green) / 255.0f;
const float b = static_cast<float>(blue) / 255.0f;
const float maximum = std::max({r, g, b});
const float minimum = std::min({r, g, b});
const float delta = maximum - minimum;
Hsv result;
result.v = maximum;
result.s = maximum <= 0.0f ? 0.0f : delta / maximum;
if (delta <= 0.00001f) {
result.h = 0.0f;
} else if (maximum == r) {
result.h = 60.0f * std::fmod((g - b) / delta, 6.0f);
} else if (maximum == g) {
result.h = 60.0f * (((b - r) / delta) + 2.0f);
} else {
result.h = 60.0f * (((r - g) / delta) + 4.0f);
}
if (result.h < 0.0f) result.h += 360.0f;
if (result.h >= 360.0f) result.h = std::fmod(result.h, 360.0f);
return result;
}
inline COLORREF hsvToRgb(float hue, float saturation, float value) {
hue = std::fmod(hue, 360.0f);
if (hue < 0.0f) hue += 360.0f;
saturation = std::clamp(saturation, 0.0f, 1.0f);
value = std::clamp(value, 0.0f, 1.0f);
const float chroma = value * saturation;
const float section = hue / 60.0f;
const float x = chroma * (1.0f - std::fabs(std::fmod(section, 2.0f) - 1.0f));
const float m = value - chroma;
float r = 0.0f;
float g = 0.0f;
float b = 0.0f;
if (section < 1.0f) { r = chroma; g = x; }
else if (section < 2.0f) { r = x; g = chroma; }
else if (section < 3.0f) { g = chroma; b = x; }
else if (section < 4.0f) { g = x; b = chroma; }
else if (section < 5.0f) { r = x; b = chroma; }
else { r = chroma; b = x; }
const auto channel = [m](float component) -> BYTE {
return static_cast<BYTE>(std::clamp(static_cast<int>(std::lround((component + m) * 255.0f)), 0, 255));
};
return RGB(channel(r), channel(g), channel(b));
}
inline std::uint32_t toArgb(COLORREF color, BYTE alpha) {
return (static_cast<std::uint32_t>(alpha) << 24u) |
(static_cast<std::uint32_t>(GetRValue(color)) << 16u) |
(static_cast<std::uint32_t>(GetGValue(color)) << 8u) |
static_cast<std::uint32_t>(GetBValue(color));
}
inline COLORREF colorFromArgb(std::uint32_t argb) {
return RGB((argb >> 16u) & 0xFFu, (argb >> 8u) & 0xFFu, argb & 0xFFu);
}
inline BYTE alphaFromArgb(std::uint32_t argb) {
return static_cast<BYTE>((argb >> 24u) & 0xFFu);
}
inline std::wstring hexRgba(COLORREF color, BYTE alpha) {
std::wostringstream stream;
stream << L'#' << std::uppercase << std::hex << std::setfill(L'0')
<< std::setw(2) << static_cast<unsigned int>(GetRValue(color))
<< std::setw(2) << static_cast<unsigned int>(GetGValue(color))
<< std::setw(2) << static_cast<unsigned int>(GetBValue(color))
<< std::setw(2) << static_cast<unsigned int>(alpha);
return stream.str();
}
inline bool parseHexRgba(std::wstring text, COLORREF& color, BYTE& alpha) {
if (!text.empty() && text.front() == L'#') text.erase(text.begin());
if (text.size() != 6 && text.size() != 8) return false;
for (wchar_t ch : text) {
if (!std::iswxdigit(static_cast<wint_t>(ch))) return false;
}
wchar_t* end = nullptr;
const unsigned long long value = std::wcstoull(text.c_str(), &end, 16);
if (!end || *end != L'\0') return false;
if (text.size() == 6) {
color = RGB((value >> 16u) & 0xFFu, (value >> 8u) & 0xFFu, value & 0xFFu);
alpha = 255;
} else {
color = RGB((value >> 24u) & 0xFFu, (value >> 16u) & 0xFFu, (value >> 8u) & 0xFFu);
alpha = static_cast<BYTE>(value & 0xFFu);
}
return true;
}
} // namespace color_picker_math
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#if !defined(_WIN32)
# error "NightShot targets Windows."
#endif
// The distributed build is intentionally Win64-only. Developers may explicitly
// opt into a compatibility build for testing, but build.bat never does this.
#if !defined(_WIN64) && !defined(NIGHTSHOT_ALLOW_WIN32)
# error "NightShot is Win64-only. Define NIGHTSHOT_ALLOW_WIN32 only for unsupported compatibility testing."
#endif
#ifndef UNICODE
# define UNICODE
#endif
#ifndef _UNICODE
# define _UNICODE
#endif
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "config.hpp"
#include "gif_recorder.hpp"
#include "settings.hpp"
#include <windows.h>
namespace gif_options_window {
// Native modal Win32 window. Returns true when the user presses Start.
[[nodiscard]] bool show(HWND owner, const settings::AppSettings& defaults,
gif_recorder::Options& options);
} // namespace gif_options_window
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include "config.hpp"
#include <windows.h>
#include <string>
namespace gif_recorder {
struct Options {
int fps = 30;
int quality = 8;
// 0 means manual stop. hardLimitSeconds is always enforced.
int durationSeconds = 0;
int hardLimitSeconds = 30;
bool captureCursor = true;
};
struct StartRequest {
RECT screenRect{};
Options options{};
HWND notificationTarget = nullptr;
};
struct Result {
bool success = false;
std::wstring path;
std::wstring message;
int frameCount = 0;
int elapsedMilliseconds = 0;
};
[[nodiscard]] bool start(const StartRequest& request);
void stop();
[[nodiscard]] bool active();
// Called by the main window after consuming WM_NIGHTSHOT_GIF_FINISHED.
void releaseFinished();
void shutdown();
} // namespace gif_recorder
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "config.hpp"
#include "settings.hpp"
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <string>
#include <vector>
[[nodiscard]] int findEncoderClsid(const WCHAR* mimeType, CLSID* clsid);
[[nodiscard]] bool saveBitmapAsPng(Gdiplus::Bitmap& bitmap, const std::wstring& path);
[[nodiscard]] bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality);
[[nodiscard]] bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap);
[[nodiscard]] bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output);
// Reserves a non-existing incremented file name with CREATE_NEW. The returned
// empty file belongs to the caller and may safely be filled/overwritten.
[[nodiscard]] std::wstring reserveIncrementedPath(
const std::wstring& directory, const wchar_t* prefix,
const wchar_t* extension);
[[nodiscard]] bool saveScreenshotIncremented(
Gdiplus::Bitmap& bitmap, const settings::AppSettings& configuration,
std::wstring& savedPath);
enum class PrintBitmapResult { Printed, Cancelled, Failed };
[[nodiscard]] PrintBitmapResult printBitmap(HWND owner, Gdiplus::Bitmap& bitmap);
[[nodiscard]] std::wstring choosePngPath(HWND owner);
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "config.hpp"
#include <windows.h>
namespace notifications {
// Registers the portable executable as a Start-menu notification source and
// initializes the Windows Runtime notification API. Failure is non-fatal.
[[nodiscard]] bool initialize();
[[nodiscard]] bool show(const wchar_t* title, const wchar_t* body);
[[nodiscard]] bool showScreenshotCopied();
void shutdown();
} // namespace notifications
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "capture.hpp"
#include "config.hpp"
#include <windows.h>
enum class OverlayMode {
Screenshot,
GifQuick,
};
class OverlayWindow {
public:
static HWND create(HINSTANCE instance, ScreenCapture&& capture,
HWND notificationTarget,
OverlayMode mode = OverlayMode::Screenshot);
private:
OverlayWindow(ScreenCapture&& capture, HWND notificationTarget,
OverlayMode mode);
~OverlayWindow();
OverlayWindow(const OverlayWindow&) = delete;
OverlayWindow& operator=(const OverlayWindow&) = delete;
static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT handleMessage(UINT message, WPARAM wParam, LPARAM lParam);
struct Impl;
Impl* impl_;
int dispatchDepth_ = 0;
bool deletePending_ = false;
};
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDI_APP_ICON 101
#define IDR_APP_MANIFEST 1
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "config.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace settings {
inline constexpr std::size_t kSavedColorCount = 48;
enum class ScreenshotFormat : int {
Png = 0,
Jpeg = 1,
};
struct AppSettings {
bool notificationsEnabled = true;
bool startWithWindows = true;
bool silentAdmin = false;
bool copyUploadedUrl = true;
bool openUploadedUrl = false;
bool closeEditorAfterUpload = true;
std::wstring uploadUrl;
std::wstring uploadName = L"Screenshot1";
std::wstring multipartField = L"file";
// Files are saved automatically with non-destructive incrementing names:
// Screenshot_N.png/.jpg and GIFShot_N.gif.
std::wstring saveDirectory;
ScreenshotFormat screenshotFormat = ScreenshotFormat::Png;
int jpegQuality = 92;
int gifFps = 30;
int gifQuality = 8;
int gifMaxDurationSeconds = 30;
bool gifCaptureCursor = true;
// Drawing widths are physical pixels and intentionally persist immediately
// when the mouse wheel changes the active tool.
int penWidth = 3;
int lineWidth = 3;
int arrowWidth = 3;
int rectangleWidth = 3;
int markerWidth = 15;
// ARGB values. Empty slots remain available for one-click saving from the
// in-editor 8x6 color picker.
std::array<std::uint32_t, kSavedColorCount> paletteArgb{};
std::array<bool, kSavedColorCount> paletteOccupied{};
};
[[nodiscard]] AppSettings load();
[[nodiscard]] bool save(const AppSettings& value);
[[nodiscard]] bool exists();
[[nodiscard]] std::wstring filePath();
[[nodiscard]] std::wstring defaultSaveDirectory();
[[nodiscard]] std::wstring normalizeEndpoint(std::wstring value);
} // namespace settings
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "config.hpp"
#include <windows.h>
namespace settings_window {
void show(HWND owner);
[[nodiscard]] bool handleDialogMessage(MSG* message);
[[nodiscard]] HWND window();
} // namespace settings_window
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "config.hpp"
#include <windows.h>
#include <string>
namespace startup {
[[nodiscard]] bool isProcessElevated();
// Returns true only in the elevated instance that should continue running.
// A non-elevated launch starts one elevated copy through UAC and returns false.
// When an existing NightShot window is already running, it returns false without
// producing another UAC prompt.
[[nodiscard]] bool ensureElevated(const wchar_t* mainWindowClass);
// Normal mode stores an HKCU Run entry. At logon that starts a tiny unelevated
// instance which immediately requests UAC once. Silent mode replaces it with a
// highest-privilege Task Scheduler task, so no UAC appears at logon.
[[nodiscard]] bool applyAutostart(bool enabled, bool silentAdmin,
std::wstring* error = nullptr);
} // namespace startup
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <windows.h>
namespace theme {
// Graphite / indigo palette from the supplied native GDI+ UI reference.
inline constexpr COLORREF kAccent = RGB(59, 130, 246);
inline constexpr COLORREF kAccentEnd = RGB(139, 92, 246);
inline constexpr COLORREF kAccentHover = RGB(96, 165, 250);
inline constexpr COLORREF kWindow = RGB(18, 18, 24);
inline constexpr COLORREF kPanel = RGB(24, 24, 34);
inline constexpr COLORREF kPanelTop = RGB(30, 30, 42);
inline constexpr COLORREF kPanelBottom = RGB(19, 19, 28);
inline constexpr COLORREF kPanelHover = RGB(43, 43, 57);
inline constexpr COLORREF kPanelActive = RGB(59, 70, 108);
inline constexpr COLORREF kBorder = RGB(60, 60, 80);
inline constexpr COLORREF kText = RGB(240, 240, 250);
inline constexpr COLORREF kMuted = RGB(150, 150, 170);
inline constexpr COLORREF kSelectionBorder = RGB(96, 165, 250);
// Annotation widths stay in physical pixels. Toolbar metrics are calculated
// per monitor from its effective DPI in overlay.cpp.
inline constexpr int kStrokeWidth = 3;
inline constexpr int kMarkerWidth = 15;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "config.hpp"
#include "settings.hpp"
#include <windows.h>
#include <string>
#include <vector>
namespace uploader {
struct Result {
bool success = false;
DWORD httpStatus = 0;
std::wstring responseUrl;
std::wstring message;
};
// Result ownership is transferred to the window through
// WM_NIGHTSHOT_UPLOAD_FINISHED in lParam.
[[nodiscard]] bool start(HWND notificationTarget, std::vector<BYTE>&& png,
const settings::AppSettings& configuration);
} // namespace uploader
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="amd64" name="NightShot" type="win32"/>
<description>NightShot Win64 screenshot utility</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<supportedOS Id="{4f476546-937d-4f38-a4b6-5d7fc6f4f253}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>
+31
View File
@@ -0,0 +1,31 @@
#include "../include/resource.h"
IDI_APP_ICON ICON "nightshot.ico"
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
1 VERSIONINFO
FILEVERSION 0,17,0,0
PRODUCTVERSION 0,17,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "NightSync"
VALUE "FileDescription", "NightShot Win64 screenshot utility"
VALUE "FileVersion", "0.17.0"
VALUE "InternalName", "NightShot"
VALUE "OriginalFilename", "NightShot.exe"
VALUE "ProductName", "NightShot"
VALUE "ProductVersion", "0.17.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+71
View File
@@ -0,0 +1,71 @@
#include "capture.hpp"
#include <utility>
ScreenCapture::ScreenCapture(ScreenCapture&& other) noexcept { *this = std::move(other); }
ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept {
if (this != &other) {
reset();
memoryDc = std::exchange(other.memoryDc, nullptr);
bitmap = std::exchange(other.bitmap, nullptr);
oldBitmap = std::exchange(other.oldBitmap, nullptr);
pixels = std::exchange(other.pixels, nullptr);
x = std::exchange(other.x, 0);
y = std::exchange(other.y, 0);
width = std::exchange(other.width, 0);
height = std::exchange(other.height, 0);
stride = std::exchange(other.stride, 0);
}
return *this;
}
ScreenCapture::~ScreenCapture() { reset(); }
void ScreenCapture::reset() noexcept {
if (memoryDc && oldBitmap) {
SelectObject(memoryDc, oldBitmap);
}
if (bitmap) DeleteObject(bitmap);
if (memoryDc) DeleteDC(memoryDc);
memoryDc = nullptr;
bitmap = nullptr;
oldBitmap = nullptr;
pixels = nullptr;
x = y = width = height = stride = 0;
}
ScreenCapture captureVirtualDesktop() {
ScreenCapture result;
result.x = GetSystemMetrics(SM_XVIRTUALSCREEN);
result.y = GetSystemMetrics(SM_YVIRTUALSCREEN);
result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
result.stride = result.width * 4;
if (result.width <= 0 || result.height <= 0) return result;
HDC screenDc = GetDC(nullptr);
if (!screenDc) return result;
result.memoryDc = CreateCompatibleDC(screenDc);
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = result.width;
info.bmiHeader.biHeight = -result.height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
result.bitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &result.pixels, nullptr, 0);
if (result.memoryDc && result.bitmap) {
result.oldBitmap = SelectObject(result.memoryDc, result.bitmap);
if (!BitBlt(result.memoryDc, 0, 0, result.width, result.height, screenDc,
result.x, result.y, SRCCOPY | CAPTUREBLT)) {
result.reset();
}
} else {
result.reset();
}
ReleaseDC(nullptr, screenDc);
return result;
}
+340
View File
@@ -0,0 +1,340 @@
#include "gif_options_window.hpp"
#include "resource.h"
#include <windows.h>
#include <algorithm>
#include <cstdlib>
#include <cwchar>
#include <iterator>
#include <array>
#include <string>
namespace gif_options_window {
namespace {
constexpr wchar_t kClassName[] = L"NightShotGifOptionsWindow";
constexpr int kIdFps = 4101;
constexpr int kIdQuality = 4102;
constexpr int kIdDuration = 4103;
constexpr int kIdCursor = 4104;
constexpr int kIdStart = 4105;
constexpr int kIdCancel = 4106;
struct State {
HWND owner = nullptr;
HWND window = nullptr;
HFONT font = nullptr;
UINT dpi = 96;
bool accepted = false;
bool finished = false;
gif_recorder::Options options{};
HWND group = nullptr;
HWND fpsLabel = nullptr;
HWND fps = nullptr;
HWND qualityLabel = nullptr;
HWND quality = nullptr;
HWND durationLabel = nullptr;
HWND duration = nullptr;
HWND durationHint = nullptr;
HWND cursor = nullptr;
HWND start = nullptr;
HWND cancel = nullptr;
};
ATOM g_class = 0;
int px(UINT dpi, int value) {
return MulDiv(value, static_cast<int>(dpi), 96);
}
void applyFont(HWND control, HFONT font) {
if (control) SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
}
HWND createControl(State& state, const wchar_t* cls, const wchar_t* text,
DWORD style, int id, DWORD exStyle = 0) {
HWND control = CreateWindowExW(
exStyle, cls, text, WS_CHILD | WS_VISIBLE | style,
0, 0, 0, 0, state.window,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(id)),
GetModuleHandleW(nullptr), nullptr);
applyFont(control, state.font);
return control;
}
void replaceFont(State& state, UINT dpi) {
state.dpi = dpi ? dpi : 96;
if (state.font) DeleteObject(state.font);
state.font = CreateFontW(
-MulDiv(9, static_cast<int>(state.dpi), 72), 0, 0, 0, FW_NORMAL,
FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
const std::array<HWND, 12> controls{
state.group, state.fpsLabel, state.fps, state.qualityLabel,
state.quality, state.durationLabel, state.duration,
state.durationHint, state.cursor, state.start, state.cancel,
state.window};
for (HWND control : controls) applyFont(control, state.font);
}
void addComboValue(HWND combo, const std::wstring& label, int value) {
const LRESULT index = SendMessageW(combo, CB_ADDSTRING, 0,
reinterpret_cast<LPARAM>(label.c_str()));
if (index >= 0) SendMessageW(combo, CB_SETITEMDATA, index, value);
}
void selectComboValue(HWND combo, int value) {
const int count = static_cast<int>(SendMessageW(combo, CB_GETCOUNT, 0, 0));
for (int i = 0; i < count; ++i) {
if (static_cast<int>(SendMessageW(combo, CB_GETITEMDATA, i, 0)) == value) {
SendMessageW(combo, CB_SETCURSEL, i, 0);
return;
}
}
SendMessageW(combo, CB_SETCURSEL, 0, 0);
}
int comboValue(HWND combo, int fallback) {
const LRESULT index = SendMessageW(combo, CB_GETCURSEL, 0, 0);
if (index == CB_ERR) return fallback;
const LRESULT value = SendMessageW(combo, CB_GETITEMDATA, index, 0);
return value == CB_ERR ? fallback : static_cast<int>(value);
}
void layout(State& state, int width, int height) {
const int margin = px(state.dpi, 16);
const int labelWidth = px(state.dpi, 158);
const int fieldWidth = px(state.dpi, 130);
const int rowHeight = px(state.dpi, 27);
const int lineGap = px(state.dpi, 11);
const int groupHeight = height - margin * 3 - px(state.dpi, 34);
MoveWindow(state.group, margin, margin, width - margin * 2, groupHeight, TRUE);
const int left = margin + px(state.dpi, 14);
const int fieldLeft = left + labelWidth;
int y = margin + px(state.dpi, 28);
MoveWindow(state.fpsLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
MoveWindow(state.fps, fieldLeft, y, fieldWidth, px(state.dpi, 180), TRUE);
y += rowHeight + lineGap;
MoveWindow(state.qualityLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
MoveWindow(state.quality, fieldLeft, y, fieldWidth, px(state.dpi, 220), TRUE);
y += rowHeight + lineGap;
MoveWindow(state.durationLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
MoveWindow(state.duration, fieldLeft, y, fieldWidth, rowHeight, TRUE);
y += rowHeight + px(state.dpi, 4);
MoveWindow(state.durationHint, fieldLeft, y, width - fieldLeft - margin * 2,
px(state.dpi, 34), TRUE);
y += px(state.dpi, 42);
MoveWindow(state.cursor, left, y, width - left - margin * 2, rowHeight, TRUE);
const int buttonWidth = px(state.dpi, 104);
const int buttonHeight = px(state.dpi, 31);
const int buttonsY = height - margin - buttonHeight;
MoveWindow(state.cancel, width - margin - buttonWidth, buttonsY,
buttonWidth, buttonHeight, TRUE);
MoveWindow(state.start, width - margin * 2 - buttonWidth * 2, buttonsY,
buttonWidth, buttonHeight, TRUE);
}
bool readAndValidate(State& state) {
wchar_t durationText[32]{};
GetWindowTextW(state.duration, durationText, static_cast<int>(std::size(durationText)));
wchar_t* end = nullptr;
const long duration = std::wcstol(durationText, &end, 10);
if (!end || *end != L'\0' || duration < 0 ||
duration > state.options.hardLimitSeconds) {
const std::wstring message =
L"Длительность должна быть от 0 до " +
std::to_wstring(state.options.hardLimitSeconds) +
L" секунд. 0 — остановка вручную.";
MessageBoxW(state.window, message.c_str(), L"NightShot — GIF",
MB_OK | MB_ICONWARNING);
SetFocus(state.duration);
return false;
}
state.options.fps = comboValue(state.fps, 30);
state.options.quality = comboValue(state.quality, 8);
state.options.durationSeconds = static_cast<int>(duration);
state.options.captureCursor =
SendMessageW(state.cursor, BM_GETCHECK, 0, 0) == BST_CHECKED;
return true;
}
LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
auto* state = reinterpret_cast<State*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (message == WM_NCCREATE) {
const auto* create = reinterpret_cast<const CREATESTRUCTW*>(lParam);
state = reinterpret_cast<State*>(create->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
state->window = hwnd;
}
if (!state) return DefWindowProcW(hwnd, message, wParam, lParam);
switch (message) {
case WM_CREATE: {
replaceFont(*state, GetDpiForWindow(hwnd));
state->group = createControl(*state, L"BUTTON", L"Параметры записи",
BS_GROUPBOX, 0);
state->fpsLabel = createControl(*state, L"STATIC", L"Кадров в секунду:", SS_LEFT, 0);
state->fps = createControl(*state, L"COMBOBOX", L"",
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL,
kIdFps);
for (const int fps : {15, 30, 45, 60}) {
addComboValue(state->fps, std::to_wstring(fps) + L" FPS", fps);
}
selectComboValue(state->fps, state->options.fps);
state->qualityLabel = createControl(*state, L"STATIC", L"Качество GIF:", SS_LEFT, 0);
state->quality = createControl(*state, L"COMBOBOX", L"",
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL,
kIdQuality);
for (int quality = 1; quality <= 10; ++quality) {
std::wstring label = std::to_wstring(quality) + L" / 10";
if (quality == 8) label += L" — рекомендуется";
addComboValue(state->quality, label, quality);
}
selectComboValue(state->quality, state->options.quality);
state->durationLabel = createControl(*state, L"STATIC", L"Длительность, сек:", SS_LEFT, 0);
state->duration = createControl(*state, L"EDIT",
std::to_wstring(state->options.durationSeconds).c_str(),
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP,
kIdDuration, WS_EX_CLIENTEDGE);
const std::wstring hint = L"0 — до повторного Ctrl+PrintScreen. Максимум: " +
std::to_wstring(state->options.hardLimitSeconds) + L" сек.";
state->durationHint = createControl(*state, L"STATIC", hint.c_str(), SS_LEFT, 0);
state->cursor = createControl(*state, L"BUTTON", L"Записывать курсор мыши",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCursor);
SendMessageW(state->cursor, BM_SETCHECK,
state->options.captureCursor ? BST_CHECKED : BST_UNCHECKED, 0);
state->start = createControl(*state, L"BUTTON", L"Начать запись",
BS_DEFPUSHBUTTON | WS_TABSTOP, kIdStart);
state->cancel = createControl(*state, L"BUTTON", L"Отмена",
BS_PUSHBUTTON | WS_TABSTOP, kIdCancel);
RECT client{};
GetClientRect(hwnd, &client);
layout(*state, client.right, client.bottom);
return 0;
}
case WM_SIZE:
layout(*state, LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_DPICHANGED: {
replaceFont(*state, HIWORD(wParam));
const RECT* suggested = reinterpret_cast<const RECT*>(lParam);
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
suggested->right - suggested->left,
suggested->bottom - suggested->top,
SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_COMMAND:
switch (LOWORD(wParam)) {
case kIdStart:
if (readAndValidate(*state)) {
state->accepted = true;
DestroyWindow(hwnd);
}
return 0;
case kIdCancel:
DestroyWindow(hwnd);
return 0;
}
break;
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
if (state->font) DeleteObject(state->font);
state->font = nullptr;
state->finished = true;
if (state->owner && IsWindow(state->owner)) {
EnableWindow(state->owner, TRUE);
SetForegroundWindow(state->owner);
}
return 0;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
void ensureClass() {
if (g_class) return;
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = windowProc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hIcon = static_cast<HICON>(LoadImageW(
wc.hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
0, 0, LR_DEFAULTSIZE));
if (!wc.hIcon) wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
wc.lpszClassName = kClassName;
g_class = RegisterClassExW(&wc);
}
} // namespace
bool show(HWND owner, const settings::AppSettings& defaults,
gif_recorder::Options& options) {
ensureClass();
if (!g_class) return false;
State state;
state.owner = owner;
state.options.fps = defaults.gifFps;
state.options.quality = defaults.gifQuality;
state.options.durationSeconds = 0;
state.options.hardLimitSeconds = defaults.gifMaxDurationSeconds;
state.options.captureCursor = defaults.gifCaptureCursor;
const UINT dpi = owner && IsWindow(owner) ? GetDpiForWindow(owner) : 96;
const int width = px(dpi, 440);
const int height = px(dpi, 300);
RECT work{};
MONITORINFO monitorInfo{};
monitorInfo.cbSize = sizeof(monitorInfo);
const HMONITOR monitor = MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST);
if (GetMonitorInfoW(monitor, &monitorInfo)) work = monitorInfo.rcWork;
else SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
const int x = work.left + std::max<LONG>(0, ((work.right - work.left) - width) / 2);
const int y = work.top + std::max<LONG>(0, ((work.bottom - work.top) - height) / 2);
HWND window = CreateWindowExW(
WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT | WS_EX_TOPMOST,
kClassName, L"NightShot — запись GIF",
WS_CAPTION | WS_SYSMENU | WS_POPUP,
x, y, width, height, owner, nullptr,
GetModuleHandleW(nullptr), &state);
if (!window) return false;
if (owner && IsWindow(owner)) EnableWindow(owner, FALSE);
ShowWindow(window, SW_SHOWNORMAL);
UpdateWindow(window);
SetForegroundWindow(window);
MSG message{};
while (!state.finished) {
const BOOL result = GetMessageW(&message, nullptr, 0, 0);
if (result <= 0) {
if (result == 0) PostQuitMessage(static_cast<int>(message.wParam));
break;
}
if (!IsDialogMessageW(window, &message)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
if (state.accepted) options = state.options;
return state.accepted;
}
} // namespace gif_options_window
+1062
View File
File diff suppressed because it is too large Load Diff
+450
View File
@@ -0,0 +1,450 @@
#include "image_io.hpp"
#include <commdlg.h>
#include <shlobj.h>
#include <filesystem>
#include <memory>
#include <sstream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <cstring>
#include <limits>
int findEncoderClsid(const WCHAR* mimeType, CLSID* clsid) {
UINT count = 0;
UINT bytes = 0;
if (Gdiplus::GetImageEncodersSize(&count, &bytes) != Gdiplus::Ok || bytes == 0) return -1;
std::vector<BYTE> storage(bytes);
auto* codecs = reinterpret_cast<Gdiplus::ImageCodecInfo*>(storage.data());
if (Gdiplus::GetImageEncoders(count, bytes, codecs) != Gdiplus::Ok) return -1;
for (UINT i = 0; i < count; ++i) {
if (wcscmp(codecs[i].MimeType, mimeType) == 0) {
*clsid = codecs[i].Clsid;
return static_cast<int>(i);
}
}
return -1;
}
bool saveBitmapAsPng(Gdiplus::Bitmap& bitmap, const std::wstring& path) {
CLSID png{};
if (findEncoderClsid(L"image/png", &png) < 0) return false;
return bitmap.Save(path.c_str(), &png, nullptr) == Gdiplus::Ok;
}
bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality) {
CLSID jpeg{};
if (findEncoderClsid(L"image/jpeg", &jpeg) < 0) return false;
ULONG value = static_cast<ULONG>(std::clamp(quality, 1, 100));
Gdiplus::EncoderParameters parameters{};
parameters.Count = 1;
parameters.Parameter[0].Guid = Gdiplus::EncoderQuality;
parameters.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
parameters.Parameter[0].NumberOfValues = 1;
parameters.Parameter[0].Value = &value;
return bitmap.Save(path.c_str(), &jpeg, &parameters) == Gdiplus::Ok;
}
namespace {
// MinGW defines LCS_sRGB using a multi-character literal ('sRGB'), which
// is implementation-defined and trips -Wmultichar. This is the exact DWORD
// value from wingdi.h, written explicitly so the build stays warning-free.
constexpr DWORD kLcsSrgb = 0x73524742UL;
struct OpaqueBgraImage {
UINT width = 0;
UINT height = 0;
INT stride = 0;
std::vector<BYTE> pixels;
[[nodiscard]] bool valid() const {
return width > 0 && height > 0 && stride > 0 && !pixels.empty();
}
};
OpaqueBgraImage renderOpaqueBgra(Gdiplus::Bitmap& source) {
OpaqueBgraImage result;
result.width = source.GetWidth();
result.height = source.GetHeight();
if (result.width == 0 || result.height == 0 ||
result.width > static_cast<UINT>(std::numeric_limits<INT>::max() / 4)) return result;
result.stride = static_cast<INT>(result.width * 4u);
const std::size_t byteCount = static_cast<std::size_t>(result.stride) * result.height;
if (byteCount / static_cast<std::size_t>(result.stride) != result.height) return {};
result.pixels.assign(byteCount, 0);
Gdiplus::Bitmap target(static_cast<INT>(result.width), static_cast<INT>(result.height), result.stride,
PixelFormat32bppARGB, result.pixels.data());
if (target.GetLastStatus() != Gdiplus::Ok) return {};
Gdiplus::Graphics graphics(&target);
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics.Clear(Gdiplus::Color(255, 255, 255, 255));
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeNearestNeighbor);
if (graphics.DrawImage(&source, 0, 0, static_cast<INT>(result.width),
static_cast<INT>(result.height)) != Gdiplus::Ok) return {};
graphics.Flush(Gdiplus::FlushIntentionSync);
// The screenshot is composited onto opaque white. Force alpha to 255 so
// applications that interpret CF_DIBV5 alpha do not paste a translucent
// or completely black image.
for (std::size_t offset = 3; offset < result.pixels.size(); offset += 4) {
result.pixels[offset] = 255;
}
return result;
}
HGLOBAL makeDibV5(const OpaqueBgraImage& image) {
if (!image.valid()) return nullptr;
const std::size_t pixelBytes = static_cast<std::size_t>(image.stride) * image.height;
const std::size_t totalBytes = sizeof(BITMAPV5HEADER) + pixelBytes;
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, totalBytes);
if (!memory) return nullptr;
auto* bytes = static_cast<BYTE*>(GlobalLock(memory));
if (!bytes) {
GlobalFree(memory);
return nullptr;
}
auto* header = reinterpret_cast<BITMAPV5HEADER*>(bytes);
header->bV5Size = sizeof(BITMAPV5HEADER);
header->bV5Width = static_cast<LONG>(image.width);
header->bV5Height = -static_cast<LONG>(image.height); // top-down
header->bV5Planes = 1;
header->bV5BitCount = 32;
header->bV5Compression = BI_BITFIELDS;
header->bV5SizeImage = static_cast<DWORD>(pixelBytes);
header->bV5RedMask = 0x00FF0000;
header->bV5GreenMask = 0x0000FF00;
header->bV5BlueMask = 0x000000FF;
header->bV5AlphaMask = 0xFF000000;
header->bV5CSType = kLcsSrgb;
header->bV5Intent = LCS_GM_IMAGES;
std::memcpy(bytes + sizeof(BITMAPV5HEADER), image.pixels.data(), pixelBytes);
GlobalUnlock(memory);
return memory;
}
HGLOBAL makeDib24(const OpaqueBgraImage& image) {
if (!image.valid()) return nullptr;
const std::size_t rowBytes = (static_cast<std::size_t>(image.width) * 3u + 3u) & ~std::size_t(3u);
const std::size_t pixelBytes = rowBytes * image.height;
const std::size_t totalBytes = sizeof(BITMAPINFOHEADER) + pixelBytes;
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, totalBytes);
if (!memory) return nullptr;
auto* bytes = static_cast<BYTE*>(GlobalLock(memory));
if (!bytes) {
GlobalFree(memory);
return nullptr;
}
auto* header = reinterpret_cast<BITMAPINFOHEADER*>(bytes);
header->biSize = sizeof(BITMAPINFOHEADER);
header->biWidth = static_cast<LONG>(image.width);
header->biHeight = static_cast<LONG>(image.height); // bottom-up for broad compatibility
header->biPlanes = 1;
header->biBitCount = 24;
header->biCompression = BI_RGB;
header->biSizeImage = static_cast<DWORD>(pixelBytes);
BYTE* destination = bytes + sizeof(BITMAPINFOHEADER);
for (UINT y = 0; y < image.height; ++y) {
const BYTE* sourceRow = image.pixels.data() + static_cast<std::size_t>(y) * image.stride;
BYTE* destinationRow = destination + static_cast<std::size_t>(image.height - 1u - y) * rowBytes;
for (UINT x = 0; x < image.width; ++x) {
destinationRow[x * 3u + 0u] = sourceRow[x * 4u + 0u];
destinationRow[x * 3u + 1u] = sourceRow[x * 4u + 1u];
destinationRow[x * 3u + 2u] = sourceRow[x * 4u + 2u];
}
}
GlobalUnlock(memory);
return memory;
}
HBITMAP makeClipboardBitmap(const OpaqueBgraImage& image) {
if (!image.valid()) return nullptr;
BITMAPINFO info{};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = static_cast<LONG>(image.width);
info.bmiHeader.biHeight = -static_cast<LONG>(image.height);
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
void* pixels = nullptr;
HDC screen = GetDC(nullptr);
HBITMAP bitmap = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
if (screen) ReleaseDC(nullptr, screen);
if (!bitmap || !pixels) {
if (bitmap) DeleteObject(bitmap);
return nullptr;
}
std::memcpy(pixels, image.pixels.data(), static_cast<std::size_t>(image.stride) * image.height);
return bitmap;
}
HGLOBAL makePng(OpaqueBgraImage& image) {
if (!image.valid()) return nullptr;
CLSID encoder{};
if (findEncoderClsid(L"image/png", &encoder) < 0) return nullptr;
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, 0);
if (!memory) return nullptr;
IStream* stream = nullptr;
if (FAILED(CreateStreamOnHGlobal(memory, FALSE, &stream)) || !stream) {
GlobalFree(memory);
return nullptr;
}
Gdiplus::Bitmap bitmap(static_cast<INT>(image.width), static_cast<INT>(image.height), image.stride,
PixelFormat32bppARGB, image.pixels.data());
const bool saved = bitmap.GetLastStatus() == Gdiplus::Ok &&
bitmap.Save(stream, &encoder, nullptr) == Gdiplus::Ok;
stream->Release();
if (!saved) {
GlobalFree(memory);
return nullptr;
}
return memory;
}
bool openClipboardWithRetry(HWND owner) {
for (int attempt = 0; attempt < 10; ++attempt) {
if (OpenClipboard(owner)) return true;
Sleep(12 + attempt * 4);
}
return false;
}
} // namespace
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
if (!image.valid()) return false;
HGLOBAL png = makePng(image);
HGLOBAL dibV5 = makeDibV5(image);
HGLOBAL dib = makeDib24(image);
HBITMAP nativeBitmap = makeClipboardBitmap(image);
if (!png && !dibV5 && !dib && !nativeBitmap) return false;
if (!openClipboardWithRetry(owner)) {
if (png) GlobalFree(png);
if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap);
return false;
}
if (!EmptyClipboard()) {
CloseClipboard();
if (png) GlobalFree(png);
if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap);
return false;
}
bool transferred = false;
const UINT pngFormat = RegisterClipboardFormatW(L"PNG");
if (png && pngFormat && SetClipboardData(pngFormat, png)) {
png = nullptr;
transferred = true;
}
if (dibV5 && SetClipboardData(CF_DIBV5, dibV5)) {
dibV5 = nullptr;
transferred = true;
}
if (dib && SetClipboardData(CF_DIB, dib)) {
dib = nullptr;
transferred = true;
}
if (nativeBitmap && SetClipboardData(CF_BITMAP, nativeBitmap)) {
nativeBitmap = nullptr;
transferred = true;
}
CloseClipboard();
if (png) GlobalFree(png);
if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap);
return transferred;
}
bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output) {
output.clear();
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
if (!image.valid()) return false;
CLSID encoder{};
if (findEncoderClsid(L"image/png", &encoder) < 0) return false;
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, 0);
if (!memory) return false;
IStream* stream = nullptr;
if (FAILED(CreateStreamOnHGlobal(memory, FALSE, &stream)) || !stream) {
GlobalFree(memory);
return false;
}
Gdiplus::Bitmap encoded(static_cast<INT>(image.width),
static_cast<INT>(image.height), image.stride,
PixelFormat32bppARGB, image.pixels.data());
bool ok = encoded.GetLastStatus() == Gdiplus::Ok &&
encoded.Save(stream, &encoder, nullptr) == Gdiplus::Ok;
STATSTG statistics{};
if (ok) ok = SUCCEEDED(stream->Stat(&statistics, STATFLAG_NONAME));
const ULONGLONG logicalBytes = statistics.cbSize.QuadPart;
if (ok) {
ok = logicalBytes > 0 &&
logicalBytes <= static_cast<ULONGLONG>(
std::numeric_limits<std::size_t>::max());
}
if (ok) {
const void* data = GlobalLock(memory);
if (!data) {
ok = false;
} else {
const auto* begin = static_cast<const BYTE*>(data);
output.assign(begin, begin + static_cast<std::size_t>(logicalBytes));
GlobalUnlock(memory);
}
}
stream->Release();
GlobalFree(memory);
if (!ok) output.clear();
return ok && !output.empty();
}
std::wstring reserveIncrementedPath(
const std::wstring& directory, const wchar_t* prefix,
const wchar_t* extension) {
if (directory.empty() || !prefix || !*prefix || !extension || !*extension) return {};
std::error_code error;
std::filesystem::create_directories(directory, error);
if (error) return {};
for (unsigned int number = 1; number < 1000000000u; ++number) {
std::filesystem::path candidate(directory);
candidate /= std::wstring(prefix) + std::to_wstring(number) + extension;
HANDLE file = CreateFileW(
candidate.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (file != INVALID_HANDLE_VALUE) {
CloseHandle(file);
return candidate.wstring();
}
const DWORD lastError = GetLastError();
if (lastError != ERROR_FILE_EXISTS && lastError != ERROR_ALREADY_EXISTS) return {};
}
return {};
}
bool saveScreenshotIncremented(
Gdiplus::Bitmap& bitmap, const settings::AppSettings& configuration,
std::wstring& savedPath) {
savedPath.clear();
const std::wstring directory = configuration.saveDirectory.empty()
? settings::defaultSaveDirectory() : configuration.saveDirectory;
const bool jpeg = configuration.screenshotFormat == settings::ScreenshotFormat::Jpeg;
const std::wstring path = reserveIncrementedPath(
directory, L"Screenshot_", jpeg ? L".jpg" : L".png");
if (path.empty()) return false;
const bool saved = jpeg
? saveBitmapAsJpeg(bitmap, path, configuration.jpegQuality)
: saveBitmapAsPng(bitmap, path);
if (!saved) {
DeleteFileW(path.c_str());
return false;
}
savedPath = path;
return true;
}
PrintBitmapResult printBitmap(HWND owner, Gdiplus::Bitmap& bitmap) {
PRINTDLGW dialog{};
dialog.lStructSize = sizeof(dialog);
dialog.hwndOwner = owner;
dialog.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION | PD_USEDEVMODECOPIESANDCOLLATE;
if (!PrintDlgW(&dialog)) return CommDlgExtendedError() == 0 ? PrintBitmapResult::Cancelled : PrintBitmapResult::Failed;
if (!dialog.hDC) return PrintBitmapResult::Failed;
DOCINFOW document{};
document.cbSize = sizeof(document);
document.lpszDocName = L"NightShot screenshot";
bool ok = StartDocW(dialog.hDC, &document) > 0;
if (ok) ok = StartPage(dialog.hDC) > 0;
if (ok) {
const int pageWidth = GetDeviceCaps(dialog.hDC, HORZRES);
const int pageHeight = GetDeviceCaps(dialog.hDC, VERTRES);
const int marginX = std::max(1, pageWidth / 20);
const int marginY = std::max(1, pageHeight / 20);
const int availableWidth = std::max(1, pageWidth - marginX * 2);
const int availableHeight = std::max(1, pageHeight - marginY * 2);
const double scale = std::min(static_cast<double>(availableWidth) / std::max<UINT>(1, bitmap.GetWidth()),
static_cast<double>(availableHeight) / std::max<UINT>(1, bitmap.GetHeight()));
const int drawWidth = std::max(1, static_cast<int>(bitmap.GetWidth() * scale));
const int drawHeight = std::max(1, static_cast<int>(bitmap.GetHeight() * scale));
const int x = (pageWidth - drawWidth) / 2;
const int y = (pageHeight - drawHeight) / 2;
Gdiplus::Graphics graphics(dialog.hDC);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
ok = graphics.DrawImage(&bitmap, x, y, drawWidth, drawHeight) == Gdiplus::Ok;
}
if (ok) ok = EndPage(dialog.hDC) > 0;
if (ok) ok = EndDoc(dialog.hDC) > 0;
else AbortDoc(dialog.hDC);
DeleteDC(dialog.hDC);
if (dialog.hDevMode) GlobalFree(dialog.hDevMode);
if (dialog.hDevNames) GlobalFree(dialog.hDevNames);
return ok ? PrintBitmapResult::Printed : PrintBitmapResult::Failed;
}
static std::wstring defaultFileName() {
SYSTEMTIME st{};
GetLocalTime(&st);
std::wostringstream out;
out << L"Screenshot_" << std::setfill(L'0')
<< std::setw(4) << st.wYear << L'-' << std::setw(2) << st.wMonth << L'-' << std::setw(2) << st.wDay
<< L'_' << std::setw(2) << st.wHour << L'-' << std::setw(2) << st.wMinute << L'-' << std::setw(2) << st.wSecond
<< L".png";
return out.str();
}
std::wstring choosePngPath(HWND owner) {
std::wstring initialDirectory;
PWSTR pictures = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Pictures, KF_FLAG_DEFAULT, nullptr, &pictures))) {
initialDirectory = pictures;
CoTaskMemFree(pictures);
}
std::wstring file = defaultFileName();
std::vector<wchar_t> buffer(32768, L'\0');
wcsncpy_s(buffer.data(), buffer.size(), file.c_str(), _TRUNCATE);
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = owner;
ofn.lpstrFilter = L"PNG image (*.png)\0*.png\0All files (*.*)\0*.*\0\0";
ofn.lpstrFile = buffer.data();
ofn.nMaxFile = static_cast<DWORD>(buffer.size());
ofn.lpstrInitialDir = initialDirectory.empty() ? nullptr : initialDirectory.c_str();
ofn.lpstrDefExt = L"png";
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
return GetSaveFileNameW(&ofn) ? std::wstring(buffer.data()) : std::wstring();
}
+499
View File
@@ -0,0 +1,499 @@
#include "config.hpp"
#include "app_messages.hpp"
#include "capture.hpp"
#include "gif_recorder.hpp"
#include "notifications.hpp"
#include "overlay.hpp"
#include "resource.h"
#include "settings.hpp"
#include "settings_window.hpp"
#include "startup.hpp"
#include "uploader.hpp"
#include <objidl.h>
#include <gdiplus.h>
#include <shellapi.h>
#include <windows.h>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <cwchar>
#include <filesystem>
#include <memory>
#include <string>
#include <utility>
namespace {
constexpr wchar_t kMainClass[] = L"NightShotMainWindow";
constexpr wchar_t kMutexName[] = L"Local\\NightShot.Win64.Singleton";
constexpr UINT kTrayMessage = WM_APP + 10;
constexpr UINT kStartCapture = WM_APP + 11;
constexpr UINT kToggleGifCapture = WM_APP + 19;
constexpr UINT kTrayId = 1;
constexpr int kHotkeyScreenshotId = 1;
constexpr int kHotkeyGifId = 2;
constexpr UINT kMenuCapture = 1001;
constexpr UINT kMenuGif = 1002;
constexpr UINT kMenuSettings = 1003;
constexpr UINT kMenuExit = 1004;
HWND g_mainWindow = nullptr;
HHOOK g_keyboardHook = nullptr;
UINT g_taskbarCreated = 0;
HWND g_overlay = nullptr;
bool g_hotkeysRegistered = false;
bool g_printScreenDown = false;
template <std::size_t N>
void copyTruncated(wchar_t (&destination)[N], const wchar_t* source) {
if constexpr (N == 0) return;
const wchar_t* text = source ? source : L"";
const std::size_t length =
std::min<std::size_t>(std::wcslen(text), N - 1);
std::wmemcpy(destination, text, length);
destination[length] = L'\0';
}
void addTrayIcon(HWND hwnd) {
NOTIFYICONDATAW data{};
data.cbSize = sizeof(data);
data.hWnd = hwnd;
data.uID = kTrayId;
data.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
data.uCallbackMessage = kTrayMessage;
data.hIcon = static_cast<HICON>(LoadImageW(
GetModuleHandleW(nullptr), MAKEINTRESOURCEW(IDI_APP_ICON),
IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (!data.hIcon) data.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
copyTruncated(data.szTip, L"NightShot — PrintScreen");
Shell_NotifyIconW(NIM_ADD, &data);
data.uVersion = NOTIFYICON_VERSION_4;
Shell_NotifyIconW(NIM_SETVERSION, &data);
}
void removeTrayIcon(HWND hwnd) {
NOTIFYICONDATAW data{};
data.cbSize = sizeof(data);
data.hWnd = hwnd;
data.uID = kTrayId;
Shell_NotifyIconW(NIM_DELETE, &data);
}
void showTrayNotification(HWND hwnd, const wchar_t* title, const wchar_t* body,
DWORD flags = NIIF_INFO) {
NOTIFYICONDATAW data{};
data.cbSize = sizeof(data);
data.hWnd = hwnd;
data.uID = kTrayId;
data.uFlags = NIF_INFO;
data.dwInfoFlags = flags | NIIF_NOSOUND | NIIF_RESPECT_QUIET_TIME;
copyTruncated(data.szInfoTitle, title ? title : L"NightShot");
copyTruncated(data.szInfo, body ? body : L"");
Shell_NotifyIconW(NIM_MODIFY, &data);
}
void notifyIfEnabled(HWND hwnd, const wchar_t* title, const wchar_t* body,
DWORD fallbackFlags = NIIF_INFO) {
if (!settings::load().notificationsEnabled) return;
if (!notifications::show(title, body)) {
showTrayNotification(hwnd, title, body, fallbackFlags);
}
}
bool copyTextToClipboard(HWND owner, const std::wstring& text) {
if (text.empty()) return false;
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (!memory) return false;
void* destination = GlobalLock(memory);
if (!destination) {
GlobalFree(memory);
return false;
}
std::memcpy(destination, text.c_str(), bytes);
GlobalUnlock(memory);
bool opened = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (OpenClipboard(owner)) {
opened = true;
break;
}
Sleep(10 + attempt * 4);
}
if (!opened) {
GlobalFree(memory);
return false;
}
bool transferred = false;
if (EmptyClipboard() && SetClipboardData(CF_UNICODETEXT, memory)) {
memory = nullptr;
transferred = true;
}
CloseClipboard();
if (memory) GlobalFree(memory);
return transferred;
}
void showTrayMenu(HWND hwnd) {
POINT cursor{};
GetCursorPos(&cursor);
HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, kMenuCapture, L"Сделать снимок\tPrintScreen");
AppendMenuW(menu, MF_STRING, kMenuGif, gif_recorder::active()
? L"Остановить GIF\tCtrl+PrintScreen"
: L"Записать GIF\tCtrl+PrintScreen");
AppendMenuW(menu, MF_STRING, kMenuSettings, L"Настройки...");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, kMenuExit, L"Выход");
SetForegroundWindow(hwnd);
const UINT command = TrackPopupMenu(
menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_NONOTIFY,
cursor.x, cursor.y, 0, hwnd, nullptr);
DestroyMenu(menu);
if (command) PostMessageW(hwnd, WM_COMMAND, command, 0);
}
void beginCapture(HWND owner, OverlayMode mode = OverlayMode::Screenshot) {
if (gif_recorder::active()) {
notifyIfEnabled(owner, L"NightShot",
L"Сначала останови текущую GIF-запись.", NIIF_WARNING);
return;
}
if (g_overlay && IsWindow(g_overlay)) {
SetForegroundWindow(g_overlay);
return;
}
ScreenCapture capture = captureVirtualDesktop();
if (!capture.valid()) {
MessageBoxW(owner, L"Не удалось захватить виртуальный рабочий стол.",
L"NightShot", MB_ICONERROR);
return;
}
g_overlay = OverlayWindow::create(
GetModuleHandleW(nullptr), std::move(capture), owner, mode);
if (!g_overlay) {
MessageBoxW(owner, L"Не удалось создать окно редактора.",
L"NightShot", MB_ICONERROR);
}
}
void handleUploadFinished(HWND hwnd, uploader::Result* rawResult) {
std::unique_ptr<uploader::Result> result(rawResult);
if (!result) return;
if (!result->success) {
std::wstring message = result->message.empty()
? L"Неизвестная ошибка загрузки."
: result->message;
MessageBoxW(hwnd, message.c_str(), L"NightShot — загрузка",
MB_OK | MB_ICONERROR);
return;
}
const settings::AppSettings configuration = settings::load();
if (!result->responseUrl.empty()) {
if (configuration.copyUploadedUrl) {
copyTextToClipboard(hwnd, result->responseUrl);
}
if (configuration.openUploadedUrl) {
ShellExecuteW(hwnd, L"open", result->responseUrl.c_str(),
nullptr, nullptr, SW_SHOWNORMAL);
}
}
std::wstring body = L"Снимок загружен";
if (!result->responseUrl.empty()) body += L"\n" + result->responseUrl;
else if (!result->message.empty() && result->message != L"Снимок загружен.") {
body += L"\n" + result->message;
}
notifyIfEnabled(hwnd, L"NightShot", body.c_str());
}
void toggleGifCapture(HWND owner) {
if (gif_recorder::active()) {
gif_recorder::stop();
return;
}
beginCapture(owner, OverlayMode::GifQuick);
}
void handleGifStart(HWND hwnd, gif_recorder::StartRequest* rawRequest) {
std::unique_ptr<gif_recorder::StartRequest> request(rawRequest);
if (!request) return;
if (!gif_recorder::start(*request)) {
MessageBoxW(hwnd, L"Не удалось начать GIF-запись. Возможно, запись уже активна.",
L"NightShot — GIF", MB_OK | MB_ICONERROR);
return;
}
notifyIfEnabled(hwnd, L"NightShot",
L"GIF-запись началась. Ctrl+PrintScreen — остановить.");
}
void handleGifFinished(HWND hwnd, gif_recorder::Result* rawResult) {
std::unique_ptr<gif_recorder::Result> result(rawResult);
if (!result) {
gif_recorder::releaseFinished();
return;
}
if (!result->success) {
const std::wstring message = result->message.empty()
? L"Неизвестная ошибка GIF-записи." : result->message;
MessageBoxW(hwnd, message.c_str(), L"NightShot — GIF",
MB_OK | MB_ICONERROR);
} else {
std::filesystem::path path(result->path);
std::wstring body = path.filename().wstring() + L" сохранён";
if (!result->message.empty()) body += L"\n" + result->message;
notifyIfEnabled(hwnd, L"NightShot — GIF", body.c_str());
}
gif_recorder::releaseFinished();
}
void handleScreenshotSaved(HWND hwnd, std::wstring* rawPath) {
std::unique_ptr<std::wstring> path(rawPath);
if (!path || path->empty()) return;
const std::wstring fileName = std::filesystem::path(*path).filename().wstring();
notifyIfEnabled(hwnd, L"NightShot",
(fileName + L" сохранён").c_str());
}
LRESULT CALLBACK keyboardProc(int code, WPARAM wParam, LPARAM lParam) {
if (code == HC_ACTION) {
const auto* key = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if (key->vkCode == VK_SNAPSHOT && !(key->flags & LLKHF_INJECTED)) {
if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
g_printScreenDown = false;
return 1;
}
if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) &&
!g_printScreenDown) {
g_printScreenDown = true;
const bool control =
(GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
if (g_mainWindow) {
PostMessageW(g_mainWindow,
control ? kToggleGifCapture : kStartCapture,
0, 0);
}
}
return 1;
}
}
return CallNextHookEx(g_keyboardHook, code, wParam, lParam);
}
LRESULT CALLBACK mainWindowProc(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam) {
if (message == g_taskbarCreated && g_taskbarCreated != 0) {
addTrayIcon(hwnd);
return 0;
}
switch (message) {
case WM_CREATE:
addTrayIcon(hwnd);
return 0;
case WM_HOTKEY:
if (wParam == kHotkeyScreenshotId) beginCapture(hwnd);
else if (wParam == kHotkeyGifId) toggleGifCapture(hwnd);
return 0;
case kStartCapture:
beginCapture(hwnd);
return 0;
case kToggleGifCapture:
toggleGifCapture(hwnd);
return 0;
case WM_NIGHTSHOT_SCREENSHOT_COPIED:
notifyIfEnabled(hwnd, L"NightShot",
L"Снимок экрана скопирован в буфер обмена");
return 0;
case WM_NIGHTSHOT_OPEN_SETTINGS:
settings_window::show(hwnd);
return 0;
case WM_NIGHTSHOT_SETTINGS_CHANGED:
return 0;
case WM_NIGHTSHOT_UPLOAD_FINISHED:
handleUploadFinished(
hwnd, reinterpret_cast<uploader::Result*>(lParam));
return 0;
case WM_NIGHTSHOT_BEGIN_GIF:
handleGifStart(
hwnd, reinterpret_cast<gif_recorder::StartRequest*>(lParam));
return 0;
case WM_NIGHTSHOT_GIF_FINISHED:
handleGifFinished(
hwnd, reinterpret_cast<gif_recorder::Result*>(lParam));
return 0;
case WM_NIGHTSHOT_SCREENSHOT_SAVED:
handleScreenshotSaved(
hwnd, reinterpret_cast<std::wstring*>(lParam));
return 0;
case kTrayMessage:
switch (LOWORD(lParam)) {
case WM_LBUTTONDBLCLK:
case NIN_SELECT:
beginCapture(hwnd);
break;
case WM_CONTEXTMENU:
case WM_RBUTTONUP:
showTrayMenu(hwnd);
break;
}
return 0;
case WM_COMMAND:
switch (LOWORD(wParam)) {
case kMenuCapture:
beginCapture(hwnd);
return 0;
case kMenuGif:
toggleGifCapture(hwnd);
return 0;
case kMenuSettings:
settings_window::show(hwnd);
return 0;
case kMenuExit:
DestroyWindow(hwnd);
return 0;
}
break;
case WM_DESTROY:
if (settings_window::window() &&
IsWindow(settings_window::window())) {
DestroyWindow(settings_window::window());
}
if (g_hotkeysRegistered) {
UnregisterHotKey(hwnd, kHotkeyScreenshotId);
UnregisterHotKey(hwnd, kHotkeyGifId);
g_hotkeysRegistered = false;
}
gif_recorder::shutdown();
removeTrayIcon(hwnd);
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
void enablePerMonitorDpi() {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
} // namespace
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) {
enablePerMonitorDpi();
// Normal startup is deliberately asInvoker first. It asks for UAC once,
// starts the elevated tray process, and exits. Silent Admin starts this
// executable elevated through Task Scheduler and skips the prompt.
if (!startup::ensureElevated(kMainClass)) return 0;
HANDLE mutex = CreateMutexW(nullptr, TRUE, kMutexName);
if (!mutex) return 1;
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(mutex);
return 0;
}
const HRESULT comResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(comResult)) {
CloseHandle(mutex);
return 2;
}
const bool firstRun = !settings::exists();
const settings::AppSettings initialSettings = settings::load();
if (firstRun) {
if (!settings::save(initialSettings)) {
MessageBoxW(nullptr, L"Не удалось создать settings.ini в AppData.",
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
}
std::wstring startupError;
if (!startup::applyAutostart(initialSettings.startWithWindows,
initialSettings.silentAdmin,
&startupError)) {
MessageBoxW(nullptr, startupError.c_str(),
L"NightShot — автозагрузка", MB_OK | MB_ICONWARNING);
}
}
[[maybe_unused]] const bool notificationRegistrationReady =
notifications::initialize();
Gdiplus::GdiplusStartupInput gdiplusInput;
ULONG_PTR gdiplusToken = 0;
if (Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusInput, nullptr) !=
Gdiplus::Ok) {
notifications::shutdown();
CoUninitialize();
CloseHandle(mutex);
return 3;
}
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = mainWindowProc;
wc.hInstance = instance;
wc.hIcon = static_cast<HICON>(LoadImageW(
instance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
0, 0, LR_DEFAULTSIZE));
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.lpszClassName = kMainClass;
if (!RegisterClassExW(&wc)) {
Gdiplus::GdiplusShutdown(gdiplusToken);
notifications::shutdown();
CoUninitialize();
CloseHandle(mutex);
return 4;
}
g_taskbarCreated = RegisterWindowMessageW(L"TaskbarCreated");
g_mainWindow = CreateWindowExW(
0, kMainClass, L"NightShot", WS_OVERLAPPED,
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
if (!g_mainWindow) {
Gdiplus::GdiplusShutdown(gdiplusToken);
notifications::shutdown();
CoUninitialize();
CloseHandle(mutex);
return 5;
}
// The low-level hook can distinguish PrintScreen from Ctrl+PrintScreen and
// also works when Windows has reserved PrintScreen for Snipping Tool.
g_keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyboardProc, instance, 0);
if (!g_keyboardHook) {
const BOOL screenshot = RegisterHotKey(
g_mainWindow, kHotkeyScreenshotId, MOD_NOREPEAT, VK_SNAPSHOT);
const BOOL gif = RegisterHotKey(
g_mainWindow, kHotkeyGifId, MOD_CONTROL | MOD_NOREPEAT, VK_SNAPSHOT);
g_hotkeysRegistered = screenshot || gif;
}
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (!settings_window::handleDialogMessage(&message)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
if (g_overlay && !IsWindow(g_overlay)) g_overlay = nullptr;
}
if (g_keyboardHook) UnhookWindowsHookEx(g_keyboardHook);
g_keyboardHook = nullptr;
g_mainWindow = nullptr;
Gdiplus::GdiplusShutdown(gdiplusToken);
notifications::shutdown();
CoUninitialize();
ReleaseMutex(mutex);
CloseHandle(mutex);
return static_cast<int>(message.wParam);
}
+199
View File
@@ -0,0 +1,199 @@
#include "notifications.hpp"
#include <roapi.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <windows.data.xml.dom.h>
#include <windows.ui.notifications.h>
#include <wrl/client.h>
#include <wrl/wrappers/corewrappers.h>
#include <string>
#include <vector>
namespace notifications {
namespace {
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::Wrappers::HStringReference;
using ABI::Windows::Data::Xml::Dom::IXmlDocument;
using ABI::Windows::Data::Xml::Dom::IXmlNode;
using ABI::Windows::Data::Xml::Dom::IXmlNodeList;
using ABI::Windows::Data::Xml::Dom::IXmlText;
using ABI::Windows::UI::Notifications::IToastNotification;
using ABI::Windows::UI::Notifications::IToastNotificationFactory;
using ABI::Windows::UI::Notifications::IToastNotificationManagerStatics;
using ABI::Windows::UI::Notifications::IToastNotifier;
constexpr wchar_t kAppUserModelId[] = L"NightSync.NightShot";
constexpr wchar_t kShortcutName[] = L"NightShot.lnk";
constexpr wchar_t kLegacyShortcutName[] = L"SnapLight.lnk";
constexpr wchar_t kToastTitle[] = L"NightShot";
constexpr wchar_t kToastBody[] = L"Снимок экрана скопирован в буфер обмена";
constexpr PROPERTYKEY kAppUserModelIdKey = {
{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}},
5};
bool g_runtimeInitialized = false;
bool g_shortcutReady = false;
std::wstring executablePath() {
std::vector<wchar_t> buffer(32768, L'\0');
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0 || length >= buffer.size()) return {};
return std::wstring(buffer.data(), length);
}
std::wstring parentDirectory(const std::wstring& path) {
const std::wstring::size_type slash = path.find_last_of(L"\\/");
return slash == std::wstring::npos ? std::wstring() : path.substr(0, slash);
}
HRESULT installOrRefreshShortcut() {
PWSTR programsPathRaw = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Programs, KF_FLAG_CREATE, nullptr, &programsPathRaw);
if (FAILED(hr) || !programsPathRaw) return FAILED(hr) ? hr : E_FAIL;
std::wstring programsPath(programsPathRaw);
CoTaskMemFree(programsPathRaw);
if (!programsPath.empty() && programsPath.back() != L'\\') programsPath.push_back(L'\\');
const std::wstring shortcutPath = programsPath + kShortcutName;
const std::wstring legacyShortcutPath = programsPath + kLegacyShortcutName;
const std::wstring exePath = executablePath();
if (exePath.empty()) return HRESULT_FROM_WIN32(GetLastError());
ComPtr<IShellLinkW> shellLink;
hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(shellLink.ReleaseAndGetAddressOf()));
if (FAILED(hr)) return hr;
hr = shellLink->SetPath(exePath.c_str());
if (FAILED(hr)) return hr;
hr = shellLink->SetArguments(L"");
if (FAILED(hr)) return hr;
hr = shellLink->SetDescription(L"NightShot screenshot utility");
if (FAILED(hr)) return hr;
const std::wstring workingDirectory = parentDirectory(exePath);
if (!workingDirectory.empty()) {
hr = shellLink->SetWorkingDirectory(workingDirectory.c_str());
if (FAILED(hr)) return hr;
}
shellLink->SetIconLocation(exePath.c_str(), 0);
ComPtr<IPropertyStore> propertyStore;
hr = shellLink.As(&propertyStore);
if (FAILED(hr)) return hr;
std::wstring mutableAppId(kAppUserModelId);
PROPVARIANT appIdValue{};
appIdValue.vt = VT_LPWSTR;
appIdValue.pwszVal = mutableAppId.data();
hr = propertyStore->SetValue(kAppUserModelIdKey, appIdValue);
if (FAILED(hr)) return hr;
hr = propertyStore->Commit();
if (FAILED(hr)) return hr;
ComPtr<IPersistFile> persistFile;
hr = shellLink.As(&persistFile);
if (FAILED(hr)) return hr;
hr = persistFile->Save(shortcutPath.c_str(), TRUE);
if (SUCCEEDED(hr)) {
DeleteFileW(legacyShortcutPath.c_str());
SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW, legacyShortcutPath.c_str(), nullptr);
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW, shortcutPath.c_str(), nullptr);
}
return hr;
}
HRESULT appendText(IXmlDocument* document, UINT32 index, const wchar_t* value) {
if (!document || !value) return E_INVALIDARG;
ComPtr<IXmlNodeList> textNodes;
HRESULT hr = document->GetElementsByTagName(HStringReference(L"text").Get(),
textNodes.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
UINT32 count = 0;
hr = textNodes->get_Length(&count);
if (FAILED(hr) || index >= count) return FAILED(hr) ? hr : E_INVALIDARG;
ComPtr<IXmlNode> targetNode;
hr = textNodes->Item(index, targetNode.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
ComPtr<IXmlText> textNode;
hr = document->CreateTextNode(HStringReference(value).Get(), textNode.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
ComPtr<IXmlNode> textAsNode;
hr = textNode.As(&textAsNode);
if (FAILED(hr)) return hr;
ComPtr<IXmlNode> appendedNode;
return targetNode->AppendChild(textAsNode.Get(), appendedNode.ReleaseAndGetAddressOf());
}
HRESULT sendToast(const wchar_t* title, const wchar_t* body) {
ComPtr<IToastNotificationManagerStatics> toastManager;
HRESULT hr = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(),
IID_PPV_ARGS(toastManager.ReleaseAndGetAddressOf()));
if (FAILED(hr)) return hr;
ComPtr<IXmlDocument> toastXml;
hr = toastManager->GetTemplateContent(ABI::Windows::UI::Notifications::ToastTemplateType_ToastText02,
toastXml.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
hr = appendText(toastXml.Get(), 0, title ? title : kToastTitle);
if (FAILED(hr)) return hr;
hr = appendText(toastXml.Get(), 1, body ? body : L"");
if (FAILED(hr)) return hr;
ComPtr<IToastNotificationFactory> toastFactory;
hr = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(),
IID_PPV_ARGS(toastFactory.ReleaseAndGetAddressOf()));
if (FAILED(hr)) return hr;
ComPtr<IToastNotification> toast;
hr = toastFactory->CreateToastNotification(toastXml.Get(), toast.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
ComPtr<IToastNotifier> notifier;
hr = toastManager->CreateToastNotifierWithId(HStringReference(kAppUserModelId).Get(),
notifier.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
return notifier->Show(toast.Get());
}
} // namespace
bool initialize() {
const HRESULT appIdResult = SetCurrentProcessExplicitAppUserModelID(kAppUserModelId);
const HRESULT runtimeResult = RoInitialize(RO_INIT_SINGLETHREADED);
g_runtimeInitialized = SUCCEEDED(runtimeResult);
g_shortcutReady = SUCCEEDED(installOrRefreshShortcut());
return SUCCEEDED(appIdResult) && g_runtimeInitialized && g_shortcutReady;
}
bool show(const wchar_t* title, const wchar_t* body) {
if (!g_runtimeInitialized || !g_shortcutReady) return false;
return SUCCEEDED(sendToast(title, body));
}
bool showScreenshotCopied() {
return show(kToastTitle, kToastBody);
}
void shutdown() {
if (g_runtimeInitialized) {
RoUninitialize();
g_runtimeInitialized = false;
}
g_shortcutReady = false;
}
} // namespace notifications
+3371
View File
File diff suppressed because it is too large Load Diff
+275
View File
@@ -0,0 +1,275 @@
#include "settings.hpp"
#include <shlobj.h>
#include <windows.h>
#include <algorithm>
#include <cwctype>
#include <cstdlib>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <vector>
namespace settings {
namespace {
std::wstring trim(std::wstring value) {
const auto isSpace = [](wchar_t ch) {
return std::iswspace(static_cast<wint_t>(ch)) != 0;
};
value.erase(value.begin(), std::find_if_not(value.begin(), value.end(), isSpace));
value.erase(std::find_if_not(value.rbegin(), value.rend(), isSpace).base(), value.end());
return value;
}
std::wstring readString(const wchar_t* section, const wchar_t* key,
const wchar_t* fallback, const std::wstring& path) {
std::vector<wchar_t> buffer(8192, L'\0');
const DWORD length = GetPrivateProfileStringW(
section, key, fallback, buffer.data(), static_cast<DWORD>(buffer.size()), path.c_str());
return std::wstring(buffer.data(), length);
}
bool readBool(const wchar_t* section, const wchar_t* key, bool fallback,
const std::wstring& path) {
return GetPrivateProfileIntW(section, key, fallback ? 1 : 0, path.c_str()) != 0;
}
bool writeBool(const wchar_t* section, const wchar_t* key, bool value,
const std::wstring& path) {
return WritePrivateProfileStringW(section, key, value ? L"1" : L"0", path.c_str()) != FALSE;
}
int readInt(const wchar_t* section, const wchar_t* key, int fallback,
const std::wstring& path, int minimum, int maximum) {
const int value = static_cast<int>(GetPrivateProfileIntW(section, key, fallback, path.c_str()));
return std::clamp(value, minimum, maximum);
}
bool writeInt(const wchar_t* section, const wchar_t* key, int value,
const std::wstring& path) {
const std::wstring text = std::to_wstring(value);
return WritePrivateProfileStringW(section, key, text.c_str(), path.c_str()) != FALSE;
}
std::wstring paletteKey(std::size_t index) {
std::wostringstream stream;
stream << L"Slot" << std::setfill(L'0') << std::setw(2) << index;
return stream.str();
}
std::wstring argbText(std::uint32_t argb) {
std::wostringstream stream;
stream << std::uppercase << std::hex << std::setfill(L'0') << std::setw(8) << argb;
return stream.str();
}
bool parseArgb(const std::wstring& text, std::uint32_t& result) {
if (text.size() != 8) return false;
wchar_t* end = nullptr;
const unsigned long value = std::wcstoul(text.c_str(), &end, 16);
if (!end || *end != L'\0') return false;
result = static_cast<std::uint32_t>(value);
return true;
}
void applyDefaultPalette(AppSettings& result) {
constexpr std::array<std::uint32_t, 8> defaults{
0xFF3B82F6u, 0xFF8B5CF6u, 0xFFEC4A5Fu, 0xFFFF9125u,
0xFFFFD541u, 0xFF48CC79u, 0xFFF5F5F5u, 0xFF191919u};
for (std::size_t i = 0; i < defaults.size(); ++i) {
result.paletteArgb[i] = defaults[i];
result.paletteOccupied[i] = true;
}
}
std::filesystem::path roamingSettingsDirectory(const wchar_t* productName) {
PWSTR roamingRaw = nullptr;
const HRESULT result = SHGetKnownFolderPath(
FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &roamingRaw);
if (FAILED(result) || !roamingRaw) return {};
std::filesystem::path directory(roamingRaw);
CoTaskMemFree(roamingRaw);
directory /= L"NightSync";
directory /= productName;
return directory;
}
bool isRegularFile(const std::filesystem::path& path) {
const DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
void migrateLegacySettings(const std::wstring& destination) {
const std::filesystem::path destinationPath(destination);
if (isRegularFile(destinationPath)) return;
std::filesystem::path legacyDirectory = roamingSettingsDirectory(L"SnapLight");
if (legacyDirectory.empty()) return;
const std::filesystem::path legacyPath = legacyDirectory / L"settings.ini";
if (!isRegularFile(legacyPath)) return;
std::error_code ignored;
std::filesystem::create_directories(destinationPath.parent_path(), ignored);
CopyFileW(legacyPath.c_str(), destinationPath.c_str(), TRUE);
}
} // namespace
std::wstring defaultSaveDirectory() {
PWSTR picturesRaw = nullptr;
const HRESULT result = SHGetKnownFolderPath(
FOLDERID_Pictures, KF_FLAG_CREATE, nullptr, &picturesRaw);
if (SUCCEEDED(result) && picturesRaw) {
std::filesystem::path directory(picturesRaw);
CoTaskMemFree(picturesRaw);
directory /= L"NightShot";
return directory.wstring();
}
wchar_t fallback[MAX_PATH]{};
const DWORD length = GetEnvironmentVariableW(L"USERPROFILE", fallback, MAX_PATH);
std::filesystem::path directory;
if (length > 0 && length < MAX_PATH) directory = fallback;
else directory = L".";
directory /= L"Pictures";
directory /= L"NightShot";
return directory.wstring();
}
std::wstring filePath() {
std::filesystem::path directory = roamingSettingsDirectory(L"NightShot");
if (directory.empty()) return L"NightShot.ini";
std::error_code ignored;
std::filesystem::create_directories(directory, ignored);
directory /= L"settings.ini";
return directory.wstring();
}
bool exists() {
const DWORD attributes = GetFileAttributesW(filePath().c_str());
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
std::wstring normalizeEndpoint(std::wstring value) {
value = trim(std::move(value));
if (value.empty()) return value;
if (value.find(L"://") == std::wstring::npos) value.insert(0, L"https://");
return value;
}
AppSettings load() {
const std::wstring path = filePath();
migrateLegacySettings(path);
AppSettings result;
result.notificationsEnabled = readBool(L"General", L"Notifications", true, path);
result.startWithWindows = readBool(L"General", L"StartWithWindows", true, path);
result.silentAdmin = readBool(L"General", L"SilentAdmin", false, path);
result.copyUploadedUrl = readBool(L"Upload", L"CopyUrl", true, path);
result.openUploadedUrl = readBool(L"Upload", L"OpenUrl", false, path);
result.closeEditorAfterUpload = readBool(L"Upload", L"CloseEditor", true, path);
result.uploadUrl = normalizeEndpoint(readString(L"Upload", L"Url", L"", path));
result.uploadName = trim(readString(L"Upload", L"Name", L"Screenshot1", path));
result.multipartField = trim(readString(L"Upload", L"MultipartField", L"file", path));
result.saveDirectory = trim(readString(
L"Saving", L"Directory", defaultSaveDirectory().c_str(), path));
if (result.saveDirectory.empty()) result.saveDirectory = defaultSaveDirectory();
result.screenshotFormat = readInt(L"Saving", L"ScreenshotFormat", 0, path, 0, 1) == 1
? ScreenshotFormat::Jpeg : ScreenshotFormat::Png;
result.jpegQuality = readInt(L"Saving", L"JpegQuality", 92, path, 1, 100);
const int loadedFps = readInt(L"GIF", L"Fps", 30, path, 15, 60);
constexpr std::array<int, 4> allowedFps{15, 30, 45, 60};
result.gifFps = *std::min_element(
allowedFps.begin(), allowedFps.end(),
[loadedFps](int a, int b) { return std::abs(a - loadedFps) < std::abs(b - loadedFps); });
result.gifQuality = readInt(L"GIF", L"Quality", 8, path, 1, 10);
result.gifMaxDurationSeconds = readInt(L"GIF", L"MaxDurationSeconds", 30, path, 1, 3600);
result.gifCaptureCursor = readBool(L"GIF", L"CaptureCursor", true, path);
result.penWidth = readInt(L"Drawing", L"PenWidth", 3, path, 1, 64);
result.lineWidth = readInt(L"Drawing", L"LineWidth", 3, path, 1, 64);
result.arrowWidth = readInt(L"Drawing", L"ArrowWidth", 3, path, 1, 64);
result.rectangleWidth = readInt(L"Drawing", L"RectangleWidth", 3, path, 1, 64);
result.markerWidth = readInt(L"Drawing", L"MarkerWidth", 15, path, 1, 96);
applyDefaultPalette(result);
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
const std::wstring key = paletteKey(i);
const std::wstring value = trim(readString(L"Palette", key.c_str(), L"", path));
if (value.empty() || value == L"-") {
if (i >= 8) result.paletteOccupied[i] = false;
continue;
}
std::uint32_t argb = 0;
if (parseArgb(value, argb)) {
result.paletteArgb[i] = argb;
result.paletteOccupied[i] = true;
}
}
if (result.uploadName.empty()) result.uploadName = L"Screenshot1";
if (result.multipartField.empty()) result.multipartField = L"file";
if (!result.startWithWindows) result.silentAdmin = false;
return result;
}
bool save(const AppSettings& value) {
const std::wstring path = filePath();
const std::wstring endpoint = normalizeEndpoint(value.uploadUrl);
const std::wstring trimmedName = trim(value.uploadName);
const std::wstring trimmedField = trim(value.multipartField);
const std::wstring name = trimmedName.empty() ? L"Screenshot1" : trimmedName;
const std::wstring field = trimmedField.empty() ? L"file" : trimmedField;
bool ok = true;
ok = writeBool(L"General", L"Notifications", value.notificationsEnabled, path) && ok;
ok = writeBool(L"General", L"StartWithWindows", value.startWithWindows, path) && ok;
ok = writeBool(L"General", L"SilentAdmin",
value.startWithWindows && value.silentAdmin, path) && ok;
ok = writeBool(L"Upload", L"CopyUrl", value.copyUploadedUrl, path) && ok;
ok = writeBool(L"Upload", L"OpenUrl", value.openUploadedUrl, path) && ok;
ok = writeBool(L"Upload", L"CloseEditor", value.closeEditorAfterUpload, path) && ok;
ok = WritePrivateProfileStringW(L"Upload", L"Url", endpoint.c_str(), path.c_str()) != FALSE && ok;
ok = WritePrivateProfileStringW(L"Upload", L"Name", name.c_str(), path.c_str()) != FALSE && ok;
ok = WritePrivateProfileStringW(L"Upload", L"MultipartField", field.c_str(), path.c_str()) != FALSE && ok;
const std::wstring directory = trim(value.saveDirectory).empty()
? defaultSaveDirectory() : trim(value.saveDirectory);
ok = WritePrivateProfileStringW(L"Saving", L"Directory", directory.c_str(), path.c_str()) != FALSE && ok;
ok = writeInt(L"Saving", L"ScreenshotFormat",
value.screenshotFormat == ScreenshotFormat::Jpeg ? 1 : 0, path) && ok;
ok = writeInt(L"Saving", L"JpegQuality", std::clamp(value.jpegQuality, 1, 100), path) && ok;
const int fps = value.gifFps == 15 || value.gifFps == 45 || value.gifFps == 60
? value.gifFps : 30;
ok = writeInt(L"GIF", L"Fps", fps, path) && ok;
ok = writeInt(L"GIF", L"Quality", std::clamp(value.gifQuality, 1, 10), path) && ok;
ok = writeInt(L"GIF", L"MaxDurationSeconds",
std::clamp(value.gifMaxDurationSeconds, 1, 3600), path) && ok;
ok = writeBool(L"GIF", L"CaptureCursor", value.gifCaptureCursor, path) && ok;
ok = writeInt(L"Drawing", L"PenWidth", std::clamp(value.penWidth, 1, 64), path) && ok;
ok = writeInt(L"Drawing", L"LineWidth", std::clamp(value.lineWidth, 1, 64), path) && ok;
ok = writeInt(L"Drawing", L"ArrowWidth", std::clamp(value.arrowWidth, 1, 64), path) && ok;
ok = writeInt(L"Drawing", L"RectangleWidth", std::clamp(value.rectangleWidth, 1, 64), path) && ok;
ok = writeInt(L"Drawing", L"MarkerWidth", std::clamp(value.markerWidth, 1, 96), path) && ok;
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
const std::wstring key = paletteKey(i);
const std::wstring slot = value.paletteOccupied[i] ? argbText(value.paletteArgb[i]) : L"-";
ok = WritePrivateProfileStringW(L"Palette", key.c_str(), slot.c_str(), path.c_str()) != FALSE && ok;
}
return ok;
}
} // namespace settings
+660
View File
@@ -0,0 +1,660 @@
#include "settings_window.hpp"
#include "app_messages.hpp"
#include "resource.h"
#include "settings.hpp"
#include "startup.hpp"
#include <shellapi.h>
#include <shlobj.h>
#include <winhttp.h>
#include <windows.h>
#include <algorithm>
#include <cstdlib>
#include <cwchar>
#include <iterator>
#include <array>
#include <filesystem>
#include <string>
#include <vector>
namespace settings_window {
namespace {
constexpr wchar_t kClassName[] = L"NightShotSettingsWindow";
constexpr int kIdNotifications = 2001;
constexpr int kIdAutostart = 2002;
constexpr int kIdSilentAdmin = 2003;
constexpr int kIdUploadUrl = 2004;
constexpr int kIdUploadName = 2005;
constexpr int kIdMultipartField = 2006;
constexpr int kIdCopyUrl = 2007;
constexpr int kIdOpenUrl = 2008;
constexpr int kIdCloseEditor = 2009;
constexpr int kIdSave = 2010;
constexpr int kIdCancel = 2011;
constexpr int kIdSaveDirectory = 2012;
constexpr int kIdBrowseDirectory = 2013;
constexpr int kIdOpenDirectory = 2014;
constexpr int kIdScreenshotFormat = 2015;
constexpr int kIdJpegQuality = 2016;
constexpr int kIdGifFps = 2017;
constexpr int kIdGifQuality = 2018;
constexpr int kIdGifLimit = 2019;
constexpr int kIdGifCursor = 2020;
struct WindowState {
HWND window = nullptr;
HWND owner = nullptr;
HFONT font = nullptr;
UINT dpi = 96;
HWND generalGroup = nullptr;
HWND notifications = nullptr;
HWND autostart = nullptr;
HWND silentAdmin = nullptr;
HWND privilegeStatus = nullptr;
HWND savingGroup = nullptr;
HWND directoryLabel = nullptr;
HWND saveDirectory = nullptr;
HWND browseDirectory = nullptr;
HWND openDirectory = nullptr;
HWND formatLabel = nullptr;
HWND screenshotFormat = nullptr;
HWND jpegQualityLabel = nullptr;
HWND jpegQuality = nullptr;
HWND gifFpsLabel = nullptr;
HWND gifFps = nullptr;
HWND gifQualityLabel = nullptr;
HWND gifQuality = nullptr;
HWND gifLimitLabel = nullptr;
HWND gifLimit = nullptr;
HWND gifLimitHint = nullptr;
HWND gifCursor = nullptr;
HWND uploadGroup = nullptr;
HWND urlLabel = nullptr;
HWND uploadUrl = nullptr;
HWND urlHint = nullptr;
HWND nameLabel = nullptr;
HWND uploadName = nullptr;
HWND fieldLabel = nullptr;
HWND multipartField = nullptr;
HWND copyUrl = nullptr;
HWND openUrl = nullptr;
HWND closeEditor = nullptr;
HWND save = nullptr;
HWND cancel = nullptr;
};
HWND g_window = nullptr;
ATOM g_class = 0;
int px(UINT dpi, int value) {
return MulDiv(value, static_cast<int>(dpi), 96);
}
void setFont(HWND control, HFONT font) {
if (control) SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
}
HWND createControl(WindowState& state, const wchar_t* className,
const wchar_t* text, DWORD style, int id,
DWORD exStyle = 0) {
HWND control = CreateWindowExW(
exStyle, className, text, WS_CHILD | WS_VISIBLE | style,
0, 0, 0, 0, state.window,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(id)),
GetModuleHandleW(nullptr), nullptr);
setFont(control, state.font);
return control;
}
std::array<HWND, 34> allControls(const WindowState& state) {
return {
state.generalGroup, state.notifications, state.autostart,
state.silentAdmin, state.privilegeStatus,
state.savingGroup, state.directoryLabel, state.saveDirectory,
state.browseDirectory, state.openDirectory, state.formatLabel,
state.screenshotFormat, state.jpegQualityLabel, state.jpegQuality,
state.gifFpsLabel, state.gifFps, state.gifQualityLabel,
state.gifQuality, state.gifLimitLabel, state.gifLimit,
state.gifLimitHint, state.gifCursor,
state.uploadGroup, state.urlLabel, state.uploadUrl, state.urlHint,
state.nameLabel, state.uploadName, state.fieldLabel,
state.multipartField, state.copyUrl, state.openUrl,
state.closeEditor, state.save
};
}
void replaceFont(WindowState& state, UINT dpi) {
state.dpi = dpi ? dpi : 96;
if (state.font) DeleteObject(state.font);
state.font = CreateFontW(
-MulDiv(9, static_cast<int>(state.dpi), 72), 0, 0, 0, FW_NORMAL,
FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
for (HWND control : allControls(state)) setFont(control, state.font);
setFont(state.cancel, state.font);
}
void layout(WindowState& state, int width, int height) {
const int margin = px(state.dpi, 16);
const int gap = px(state.dpi, 9);
const int row = px(state.dpi, 24);
const int editHeight = px(state.dpi, 27);
const int buttonWidth = px(state.dpi, 108);
const int buttonHeight = px(state.dpi, 31);
const int contentWidth = std::max(px(state.dpi, 520), width - margin * 2);
const int innerLeft = margin + px(state.dpi, 14);
const int innerWidth = contentWidth - px(state.dpi, 28);
int y = margin;
const int generalHeight = px(state.dpi, 142);
MoveWindow(state.generalGroup, margin, y, contentWidth, generalHeight, TRUE);
int gy = y + px(state.dpi, 24);
MoveWindow(state.notifications, innerLeft, gy, innerWidth, row, TRUE);
gy += row + px(state.dpi, 3);
MoveWindow(state.autostart, innerLeft, gy, innerWidth, row, TRUE);
gy += row + px(state.dpi, 3);
MoveWindow(state.silentAdmin, innerLeft + px(state.dpi, 20), gy,
innerWidth - px(state.dpi, 20), row, TRUE);
gy += row + px(state.dpi, 1);
MoveWindow(state.privilegeStatus, innerLeft + px(state.dpi, 20), gy,
innerWidth - px(state.dpi, 20), px(state.dpi, 32), TRUE);
y += generalHeight + gap;
const int savingHeight = px(state.dpi, 246);
MoveWindow(state.savingGroup, margin, y, contentWidth, savingHeight, TRUE);
const int labelWidth = px(state.dpi, 145);
int sy = y + px(state.dpi, 27);
MoveWindow(state.directoryLabel, innerLeft, sy + px(state.dpi, 4),
labelWidth, row, TRUE);
const int smallButton = px(state.dpi, 86);
const int directoryLeft = innerLeft + labelWidth;
const int directoryWidth = innerWidth - labelWidth - smallButton * 2 - px(state.dpi, 12);
MoveWindow(state.saveDirectory, directoryLeft, sy, directoryWidth, editHeight, TRUE);
MoveWindow(state.browseDirectory, directoryLeft + directoryWidth + px(state.dpi, 4), sy,
smallButton, editHeight, TRUE);
MoveWindow(state.openDirectory, directoryLeft + directoryWidth + smallButton + px(state.dpi, 8), sy,
smallButton, editHeight, TRUE);
sy += editHeight + px(state.dpi, 10);
const int comboWidth = px(state.dpi, 132);
MoveWindow(state.formatLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
MoveWindow(state.screenshotFormat, directoryLeft, sy, comboWidth, px(state.dpi, 160), TRUE);
MoveWindow(state.jpegQualityLabel, directoryLeft + comboWidth + px(state.dpi, 18),
sy + px(state.dpi, 4), px(state.dpi, 110), row, TRUE);
MoveWindow(state.jpegQuality, directoryLeft + comboWidth + px(state.dpi, 128),
sy, px(state.dpi, 62), editHeight, TRUE);
sy += editHeight + px(state.dpi, 10);
MoveWindow(state.gifFpsLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
MoveWindow(state.gifFps, directoryLeft, sy, comboWidth, px(state.dpi, 150), TRUE);
MoveWindow(state.gifQualityLabel, directoryLeft + comboWidth + px(state.dpi, 18),
sy + px(state.dpi, 4), px(state.dpi, 110), row, TRUE);
MoveWindow(state.gifQuality, directoryLeft + comboWidth + px(state.dpi, 128),
sy, px(state.dpi, 88), px(state.dpi, 220), TRUE);
sy += editHeight + px(state.dpi, 10);
MoveWindow(state.gifLimitLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
MoveWindow(state.gifLimit, directoryLeft, sy, px(state.dpi, 82), editHeight, TRUE);
MoveWindow(state.gifLimitHint, directoryLeft + px(state.dpi, 92), sy + px(state.dpi, 4),
innerWidth - labelWidth - px(state.dpi, 92), row, TRUE);
sy += editHeight + px(state.dpi, 8);
MoveWindow(state.gifCursor, innerLeft, sy, innerWidth, row, TRUE);
y += savingHeight + gap;
const int buttonsY = height - margin - buttonHeight;
const int uploadBottom = buttonsY - gap;
const int uploadHeight = std::max(px(state.dpi, 235), uploadBottom - y);
MoveWindow(state.uploadGroup, margin, y, contentWidth, uploadHeight, TRUE);
const int uploadLabelWidth = px(state.dpi, 126);
const int uploadEditLeft = innerLeft + uploadLabelWidth;
const int uploadEditWidth = innerWidth - uploadLabelWidth;
int uy = y + px(state.dpi, 27);
MoveWindow(state.urlLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
MoveWindow(state.uploadUrl, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
uy += editHeight + px(state.dpi, 4);
MoveWindow(state.urlHint, uploadEditLeft, uy, uploadEditWidth, px(state.dpi, 34), TRUE);
uy += px(state.dpi, 42);
MoveWindow(state.nameLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
MoveWindow(state.uploadName, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
uy += editHeight + px(state.dpi, 7);
MoveWindow(state.fieldLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
MoveWindow(state.multipartField, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
uy += editHeight + px(state.dpi, 9);
MoveWindow(state.copyUrl, innerLeft, uy, innerWidth, row, TRUE);
uy += row + px(state.dpi, 1);
MoveWindow(state.openUrl, innerLeft, uy, innerWidth, row, TRUE);
uy += row + px(state.dpi, 1);
MoveWindow(state.closeEditor, innerLeft, uy, innerWidth, row, TRUE);
MoveWindow(state.cancel, width - margin - buttonWidth, buttonsY,
buttonWidth, buttonHeight, TRUE);
MoveWindow(state.save, width - margin * 2 - buttonWidth * 2, buttonsY,
buttonWidth, buttonHeight, TRUE);
}
void setCheck(HWND control, bool checked) {
SendMessageW(control, BM_SETCHECK, checked ? BST_CHECKED : BST_UNCHECKED, 0);
}
bool getCheck(HWND control) {
return SendMessageW(control, BM_GETCHECK, 0, 0) == BST_CHECKED;
}
std::wstring getText(HWND control) {
const int length = GetWindowTextLengthW(control);
std::vector<wchar_t> buffer(static_cast<std::size_t>(std::max(0, length)) + 1, L'\0');
GetWindowTextW(control, buffer.data(), static_cast<int>(buffer.size()));
return std::wstring(buffer.data());
}
void addComboValue(HWND combo, const std::wstring& label, int value) {
const LRESULT index = SendMessageW(combo, CB_ADDSTRING, 0,
reinterpret_cast<LPARAM>(label.c_str()));
if (index >= 0) SendMessageW(combo, CB_SETITEMDATA, index, value);
}
void selectComboValue(HWND combo, int value) {
const int count = static_cast<int>(SendMessageW(combo, CB_GETCOUNT, 0, 0));
for (int i = 0; i < count; ++i) {
if (static_cast<int>(SendMessageW(combo, CB_GETITEMDATA, i, 0)) == value) {
SendMessageW(combo, CB_SETCURSEL, i, 0);
return;
}
}
SendMessageW(combo, CB_SETCURSEL, 0, 0);
}
int comboValue(HWND combo, int fallback) {
const LRESULT index = SendMessageW(combo, CB_GETCURSEL, 0, 0);
if (index == CB_ERR) return fallback;
const LRESULT value = SendMessageW(combo, CB_GETITEMDATA, index, 0);
return value == CB_ERR ? fallback : static_cast<int>(value);
}
bool parseInteger(HWND control, int minimum, int maximum, int& result) {
const std::wstring text = getText(control);
wchar_t* end = nullptr;
const long value = std::wcstol(text.c_str(), &end, 10);
if (!end || *end != L'\0' || value < minimum || value > maximum) return false;
result = static_cast<int>(value);
return true;
}
void updateAutostartControls(WindowState& state) {
const bool enabled = getCheck(state.autostart);
EnableWindow(state.silentAdmin, enabled ? TRUE : FALSE);
if (!enabled) setCheck(state.silentAdmin, false);
const wchar_t* text = enabled && getCheck(state.silentAdmin)
? L"Silent Admin: вход в Windows без UAC через Планировщик."
: L"Обычный режим: при входе в Windows один UAC; во время работы повторных запросов нет.";
SetWindowTextW(state.privilegeStatus, text);
}
void updateFormatControls(WindowState& state) {
const bool jpeg = comboValue(state.screenshotFormat, 0) == 1;
EnableWindow(state.jpegQualityLabel, jpeg ? TRUE : FALSE);
EnableWindow(state.jpegQuality, jpeg ? TRUE : FALSE);
}
void populate(WindowState& state) {
const settings::AppSettings current = settings::load();
setCheck(state.notifications, current.notificationsEnabled);
setCheck(state.autostart, current.startWithWindows);
setCheck(state.silentAdmin, current.silentAdmin);
setCheck(state.copyUrl, current.copyUploadedUrl);
setCheck(state.openUrl, current.openUploadedUrl);
setCheck(state.closeEditor, current.closeEditorAfterUpload);
setCheck(state.gifCursor, current.gifCaptureCursor);
SetWindowTextW(state.uploadUrl, current.uploadUrl.c_str());
SetWindowTextW(state.uploadName, current.uploadName.c_str());
SetWindowTextW(state.multipartField, current.multipartField.c_str());
SetWindowTextW(state.saveDirectory, current.saveDirectory.c_str());
SetWindowTextW(state.jpegQuality, std::to_wstring(current.jpegQuality).c_str());
SetWindowTextW(state.gifLimit, std::to_wstring(current.gifMaxDurationSeconds).c_str());
selectComboValue(state.screenshotFormat,
current.screenshotFormat == settings::ScreenshotFormat::Jpeg ? 1 : 0);
selectComboValue(state.gifFps, current.gifFps);
selectComboValue(state.gifQuality, current.gifQuality);
updateAutostartControls(state);
updateFormatControls(state);
}
bool isValidUploadUrl(const std::wstring& url) {
if (url.empty()) return true;
URL_COMPONENTSW components{};
components.dwStructSize = sizeof(components);
components.dwSchemeLength = static_cast<DWORD>(-1);
components.dwHostNameLength = static_cast<DWORD>(-1);
components.dwUrlPathLength = static_cast<DWORD>(-1);
components.dwExtraInfoLength = static_cast<DWORD>(-1);
return WinHttpCrackUrl(url.c_str(), static_cast<DWORD>(url.size()), 0,
&components) != FALSE &&
components.dwHostNameLength > 0 &&
(components.nScheme == INTERNET_SCHEME_HTTP ||
components.nScheme == INTERNET_SCHEME_HTTPS);
}
int CALLBACK browseCallback(HWND hwnd, UINT message, LPARAM, LPARAM data) {
if (message == BFFM_INITIALIZED && data) {
SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, data);
}
return 0;
}
std::wstring chooseFolder(HWND owner, const std::wstring& initial) {
BROWSEINFOW info{};
info.hwndOwner = owner;
info.lpszTitle = L"Выбери папку для снимков и GIF";
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE | BIF_USENEWUI;
info.lpfn = browseCallback;
info.lParam = reinterpret_cast<LPARAM>(initial.c_str());
LPITEMIDLIST selected = SHBrowseForFolderW(&info);
if (!selected) return {};
wchar_t path[MAX_PATH]{};
const bool ok = SHGetPathFromIDListW(selected, path) != FALSE;
CoTaskMemFree(selected);
return ok ? std::wstring(path) : std::wstring();
}
bool saveValues(WindowState& state) {
settings::AppSettings next = settings::load();
next.notificationsEnabled = getCheck(state.notifications);
next.startWithWindows = getCheck(state.autostart);
next.silentAdmin = next.startWithWindows && getCheck(state.silentAdmin);
next.copyUploadedUrl = getCheck(state.copyUrl);
next.openUploadedUrl = getCheck(state.openUrl);
next.closeEditorAfterUpload = getCheck(state.closeEditor);
next.uploadUrl = settings::normalizeEndpoint(getText(state.uploadUrl));
next.uploadName = getText(state.uploadName);
next.multipartField = getText(state.multipartField);
next.saveDirectory = getText(state.saveDirectory);
next.screenshotFormat = comboValue(state.screenshotFormat, 0) == 1
? settings::ScreenshotFormat::Jpeg : settings::ScreenshotFormat::Png;
next.gifFps = comboValue(state.gifFps, 30);
next.gifQuality = comboValue(state.gifQuality, 8);
next.gifCaptureCursor = getCheck(state.gifCursor);
if (!isValidUploadUrl(next.uploadUrl)) {
MessageBoxW(g_window,
L"URL загрузчика должен быть обычным http:// или https:// адресом.",
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
SetFocus(state.uploadUrl);
return false;
}
if (next.saveDirectory.empty()) next.saveDirectory = settings::defaultSaveDirectory();
std::error_code directoryError;
std::filesystem::create_directories(next.saveDirectory, directoryError);
if (directoryError) {
MessageBoxW(g_window, L"Не удалось создать выбранную папку сохранения.",
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
SetFocus(state.saveDirectory);
return false;
}
if (!parseInteger(state.jpegQuality, 1, 100, next.jpegQuality)) {
MessageBoxW(g_window, L"Качество JPEG должно быть от 1 до 100.",
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
SetFocus(state.jpegQuality);
return false;
}
if (!parseInteger(state.gifLimit, 1, 3600, next.gifMaxDurationSeconds)) {
MessageBoxW(g_window, L"Лимит GIF должен быть от 1 до 3600 секунд.",
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
SetFocus(state.gifLimit);
return false;
}
std::wstring startupError;
if (!startup::applyAutostart(next.startWithWindows, next.silentAdmin,
&startupError)) {
MessageBoxW(g_window, startupError.c_str(),
L"Не удалось изменить автозагрузку", MB_OK | MB_ICONERROR);
return false;
}
if (!settings::save(next)) {
MessageBoxW(g_window, L"Не удалось сохранить settings.ini в AppData.",
L"NightShot — настройки", MB_OK | MB_ICONERROR);
return false;
}
if (state.owner && IsWindow(state.owner)) {
PostMessageW(state.owner, WM_NIGHTSHOT_SETTINGS_CHANGED, 0, 0);
}
return true;
}
LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
auto* state = reinterpret_cast<WindowState*>(
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (message == WM_NCCREATE) {
const auto* create = reinterpret_cast<const CREATESTRUCTW*>(lParam);
state = reinterpret_cast<WindowState*>(create->lpCreateParams);
if (state) state->window = hwnd;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
}
if (!state) return DefWindowProcW(hwnd, message, wParam, lParam);
switch (message) {
case WM_CREATE: {
state->dpi = GetDpiForWindow(hwnd);
replaceFont(*state, state->dpi);
state->generalGroup = createControl(*state, L"BUTTON", L"Общие", BS_GROUPBOX, 0);
state->notifications = createControl(*state, L"BUTTON",
L"Показывать системные уведомления Windows",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdNotifications);
state->autostart = createControl(*state, L"BUTTON", L"Запускать вместе с Windows",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdAutostart);
state->silentAdmin = createControl(*state, L"BUTTON",
L"Silent Admin — запуск без UAC",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdSilentAdmin);
state->privilegeStatus = createControl(*state, L"STATIC", L"", SS_LEFT, 0);
state->savingGroup = createControl(*state, L"BUTTON", L"Сохранение и GIF", BS_GROUPBOX, 0);
state->directoryLabel = createControl(*state, L"STATIC", L"Папка сохранения:", SS_LEFT, 0);
state->saveDirectory = createControl(*state, L"EDIT", L"",
WS_TABSTOP | ES_AUTOHSCROLL, kIdSaveDirectory, WS_EX_CLIENTEDGE);
state->browseDirectory = createControl(*state, L"BUTTON", L"Обзор...",
BS_PUSHBUTTON | WS_TABSTOP, kIdBrowseDirectory);
state->openDirectory = createControl(*state, L"BUTTON", L"Открыть",
BS_PUSHBUTTON | WS_TABSTOP, kIdOpenDirectory);
state->formatLabel = createControl(*state, L"STATIC", L"Формат снимков:", SS_LEFT, 0);
state->screenshotFormat = createControl(*state, L"COMBOBOX", L"",
CBS_DROPDOWNLIST | WS_TABSTOP, kIdScreenshotFormat);
addComboValue(state->screenshotFormat, L"PNG", 0);
addComboValue(state->screenshotFormat, L"JPEG", 1);
state->jpegQualityLabel = createControl(*state, L"STATIC", L"JPEG качество:", SS_LEFT, 0);
state->jpegQuality = createControl(*state, L"EDIT", L"92",
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP, kIdJpegQuality, WS_EX_CLIENTEDGE);
state->gifFpsLabel = createControl(*state, L"STATIC", L"GIF по умолчанию:", SS_LEFT, 0);
state->gifFps = createControl(*state, L"COMBOBOX", L"",
CBS_DROPDOWNLIST | WS_TABSTOP, kIdGifFps);
for (int fps : {15, 30, 45, 60}) addComboValue(state->gifFps, std::to_wstring(fps) + L" FPS", fps);
state->gifQualityLabel = createControl(*state, L"STATIC", L"GIF качество:", SS_LEFT, 0);
state->gifQuality = createControl(*state, L"COMBOBOX", L"",
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL, kIdGifQuality);
for (int quality = 1; quality <= 10; ++quality) {
addComboValue(state->gifQuality, std::to_wstring(quality) + L" / 10", quality);
}
state->gifLimitLabel = createControl(*state, L"STATIC", L"Лимит GIF, сек:", SS_LEFT, 0);
state->gifLimit = createControl(*state, L"EDIT", L"30",
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP, kIdGifLimit, WS_EX_CLIENTEDGE);
state->gifLimitHint = createControl(*state, L"STATIC",
L"Запись остановится автоматически при достижении лимита.", SS_LEFT, 0);
state->gifCursor = createControl(*state, L"BUTTON", L"Записывать курсор в GIF",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdGifCursor);
state->uploadGroup = createControl(*state, L"BUTTON", L"API-загрузка", BS_GROUPBOX, 0);
state->urlLabel = createControl(*state, L"STATIC", L"URL сервера:", SS_LEFT, 0);
state->uploadUrl = createControl(*state, L"EDIT", L"",
WS_TABSTOP | ES_AUTOHSCROLL, kIdUploadUrl, WS_EX_CLIENTEDGE);
state->urlHint = createControl(*state, L"STATIC",
L"К URL добавляется параметр name, PNG отправляется как multipart/form-data.", SS_LEFT, 0);
state->nameLabel = createControl(*state, L"STATIC", L"Параметр name:", SS_LEFT, 0);
state->uploadName = createControl(*state, L"EDIT", L"",
WS_TABSTOP | ES_AUTOHSCROLL, kIdUploadName, WS_EX_CLIENTEDGE);
state->fieldLabel = createControl(*state, L"STATIC", L"Multipart-поле:", SS_LEFT, 0);
state->multipartField = createControl(*state, L"EDIT", L"",
WS_TABSTOP | ES_AUTOHSCROLL, kIdMultipartField, WS_EX_CLIENTEDGE);
state->copyUrl = createControl(*state, L"BUTTON", L"Копировать URL из ответа сервера",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCopyUrl);
state->openUrl = createControl(*state, L"BUTTON", L"Открывать полученный URL в браузере",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdOpenUrl);
state->closeEditor = createControl(*state, L"BUTTON", L"Закрывать редактор после начала загрузки",
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCloseEditor);
state->save = createControl(*state, L"BUTTON", L"Сохранить",
BS_DEFPUSHBUTTON | WS_TABSTOP, kIdSave);
state->cancel = createControl(*state, L"BUTTON", L"Отмена",
BS_PUSHBUTTON | WS_TABSTOP, kIdCancel);
populate(*state);
RECT client{};
GetClientRect(hwnd, &client);
layout(*state, client.right, client.bottom);
return 0;
}
case WM_SIZE:
layout(*state, LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_DPICHANGED: {
replaceFont(*state, HIWORD(wParam));
const RECT* suggested = reinterpret_cast<const RECT*>(lParam);
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
suggested->right - suggested->left,
suggested->bottom - suggested->top,
SWP_NOZORDER | SWP_NOACTIVATE);
RECT client{};
GetClientRect(hwnd, &client);
layout(*state, client.right, client.bottom);
return 0;
}
case WM_GETMINMAXINFO: {
auto* info = reinterpret_cast<MINMAXINFO*>(lParam);
info->ptMinTrackSize.x = px(state->dpi, 700);
info->ptMinTrackSize.y = px(state->dpi, 755);
return 0;
}
case WM_COMMAND:
switch (LOWORD(wParam)) {
case kIdAutostart:
case kIdSilentAdmin:
updateAutostartControls(*state);
return 0;
case kIdScreenshotFormat:
if (HIWORD(wParam) == CBN_SELCHANGE) updateFormatControls(*state);
return 0;
case kIdBrowseDirectory: {
const std::wstring selected = chooseFolder(hwnd, getText(state->saveDirectory));
if (!selected.empty()) SetWindowTextW(state->saveDirectory, selected.c_str());
return 0;
}
case kIdOpenDirectory: {
std::wstring directory = getText(state->saveDirectory);
if (directory.empty()) directory = settings::defaultSaveDirectory();
std::error_code ignored;
std::filesystem::create_directories(directory, ignored);
ShellExecuteW(hwnd, L"open", directory.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return 0;
}
case kIdSave:
if (saveValues(*state)) DestroyWindow(hwnd);
return 0;
case kIdCancel:
DestroyWindow(hwnd);
return 0;
}
break;
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
if (state->font) DeleteObject(state->font);
state->font = nullptr;
g_window = nullptr;
if (state->owner && IsWindow(state->owner)) EnableWindow(state->owner, TRUE);
delete state;
return 0;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
void ensureClass() {
if (g_class) return;
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = windowProc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hIcon = static_cast<HICON>(LoadImageW(
wc.hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
0, 0, LR_DEFAULTSIZE));
if (!wc.hIcon) wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
wc.lpszClassName = kClassName;
g_class = RegisterClassExW(&wc);
}
} // namespace
void show(HWND owner) {
if (g_window && IsWindow(g_window)) {
ShowWindow(g_window, SW_RESTORE);
SetForegroundWindow(g_window);
return;
}
ensureClass();
if (!g_class) return;
auto* state = new WindowState();
state->owner = owner;
const UINT dpi = owner ? GetDpiForWindow(owner) : 96;
const int width = px(dpi, 760);
const int height = px(dpi, 790);
MONITORINFO info{};
info.cbSize = sizeof(info);
RECT work{};
const HMONITOR monitor = MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST);
if (GetMonitorInfoW(monitor, &info)) work = info.rcWork;
else SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
const LONG workWidth = work.right - work.left;
const LONG workHeight = work.bottom - work.top;
const int x = work.left + std::max<LONG>(0, (workWidth - width) / 2);
const int y = work.top + std::max<LONG>(0, (workHeight - height) / 2);
g_window = CreateWindowExW(
WS_EX_CONTROLPARENT | WS_EX_DLGMODALFRAME,
kClassName, L"NightShot — настройки",
WS_OVERLAPPEDWINDOW,
x, y, width, height, owner, nullptr,
GetModuleHandleW(nullptr), state);
if (!g_window) {
delete state;
return;
}
if (owner) EnableWindow(owner, FALSE);
ShowWindow(g_window, SW_SHOWNORMAL);
UpdateWindow(g_window);
SetForegroundWindow(g_window);
}
bool handleDialogMessage(MSG* message) {
return message && g_window && IsWindow(g_window) &&
IsDialogMessageW(g_window, message) != FALSE;
}
HWND window() {
return g_window;
}
} // namespace settings_window
+364
View File
@@ -0,0 +1,364 @@
#include "startup.hpp"
#include <sddl.h>
#include <shellapi.h>
#include <windows.h>
#include <algorithm>
#include <cwchar>
#include <string>
#include <vector>
namespace startup {
namespace {
constexpr wchar_t kRunKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
constexpr wchar_t kRunValue[] = L"NightShot";
constexpr wchar_t kTaskName[] = L"NightShot Silent Admin";
constexpr wchar_t kLegacyRunValue[] = L"SnapLight";
constexpr wchar_t kLegacyTaskName[] = L"SnapLight Silent Admin";
constexpr wchar_t kLegacyMainWindowClass[] = L"SnapLightMainWindow";
std::wstring executablePath() {
std::vector<wchar_t> buffer(32768, L'\0');
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
static_cast<DWORD>(buffer.size()));
if (length == 0 || length >= buffer.size()) return {};
return std::wstring(buffer.data(), length);
}
std::wstring parentDirectory(const std::wstring& path) {
const std::wstring::size_type slash = path.find_last_of(L"\\/");
return slash == std::wstring::npos ? std::wstring() : path.substr(0, slash);
}
std::wstring quote(const std::wstring& value) {
return L"\"" + value + L"\"";
}
std::wstring win32Error(DWORD code) {
wchar_t* raw = nullptr;
const DWORD length = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, code, 0, reinterpret_cast<wchar_t*>(&raw), 0, nullptr);
std::wstring result = length && raw
? std::wstring(raw, length)
: L"Ошибка Windows " + std::to_wstring(code);
if (raw) LocalFree(raw);
while (!result.empty() &&
(result.back() == L'\r' || result.back() == L'\n' || result.back() == L' ')) {
result.pop_back();
}
return result;
}
std::vector<std::wstring> commandLineArguments() {
int count = 0;
LPWSTR* raw = CommandLineToArgvW(GetCommandLineW(), &count);
std::vector<std::wstring> result;
if (raw) {
result.reserve(static_cast<std::size_t>(count));
for (int i = 0; i < count; ++i) result.emplace_back(raw[i]);
LocalFree(raw);
}
return result;
}
std::wstring elevationArguments() {
const std::vector<std::wstring> args = commandLineArguments();
std::wstring result;
bool hasElevatedMarker = false;
for (std::size_t i = 1; i < args.size(); ++i) {
if (args[i] == L"--elevated") {
hasElevatedMarker = true;
continue;
}
if (!result.empty()) result.push_back(L' ');
result += quote(args[i]);
}
if (!hasElevatedMarker) {
if (!result.empty()) result.push_back(L' ');
result += L"--elevated";
}
return result;
}
bool setRunValue(bool enabled, std::wstring* error) {
HKEY key = nullptr;
LONG status = RegCreateKeyExW(HKEY_CURRENT_USER, kRunKey, 0, nullptr, 0,
KEY_SET_VALUE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) {
if (error) *error = win32Error(static_cast<DWORD>(status));
return false;
}
if (enabled) {
const std::wstring exe = executablePath();
if (exe.empty()) {
RegCloseKey(key);
if (error) *error = L"Не удалось определить путь к NightShot.exe.";
return false;
}
const std::wstring command = quote(exe) + L" --startup";
status = RegSetValueExW(
key, kRunValue, 0, REG_SZ,
reinterpret_cast<const BYTE*>(command.c_str()),
static_cast<DWORD>((command.size() + 1) * sizeof(wchar_t)));
} else {
status = RegDeleteValueW(key, kRunValue);
if (status == ERROR_FILE_NOT_FOUND) status = ERROR_SUCCESS;
}
RegCloseKey(key);
if (status != ERROR_SUCCESS && error) {
*error = win32Error(static_cast<DWORD>(status));
}
return status == ERROR_SUCCESS;
}
bool deleteRunValue(const wchar_t* valueName) {
HKEY key = nullptr;
LONG status = RegOpenKeyExW(HKEY_CURRENT_USER, kRunKey, 0, KEY_SET_VALUE, &key);
if (status == ERROR_FILE_NOT_FOUND) return true;
if (status != ERROR_SUCCESS) return false;
status = RegDeleteValueW(key, valueName);
RegCloseKey(key);
return status == ERROR_SUCCESS || status == ERROR_FILE_NOT_FOUND;
}
std::wstring schtasksPath() {
std::vector<wchar_t> buffer(MAX_PATH + 1, L'\0');
const UINT length = GetSystemDirectoryW(buffer.data(),
static_cast<UINT>(buffer.size()));
if (length == 0 || length >= buffer.size()) return L"schtasks.exe";
std::wstring result(buffer.data(), length);
result += L"\\schtasks.exe";
return result;
}
DWORD runHiddenProcess(std::wstring commandLine) {
STARTUPINFOW startupInfo{};
startupInfo.cb = sizeof(startupInfo);
PROCESS_INFORMATION processInfo{};
std::vector<wchar_t> mutableCommand(commandLine.begin(), commandLine.end());
mutableCommand.push_back(L'\0');
if (!CreateProcessW(nullptr, mutableCommand.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startupInfo, &processInfo)) {
return GetLastError();
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
DWORD exitCode = ERROR_GEN_FAILURE;
GetExitCodeProcess(processInfo.hProcess, &exitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
return exitCode;
}
std::wstring currentUserSid() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return {};
DWORD bytes = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &bytes);
if (bytes == 0) {
CloseHandle(token);
return {};
}
std::vector<BYTE> storage(bytes);
if (!GetTokenInformation(token, TokenUser, storage.data(), bytes, &bytes)) {
CloseHandle(token);
return {};
}
CloseHandle(token);
const auto* user = reinterpret_cast<const TOKEN_USER*>(storage.data());
LPWSTR rawSid = nullptr;
if (!ConvertSidToStringSidW(user->User.Sid, &rawSid) || !rawSid) return {};
std::wstring result(rawSid);
LocalFree(rawSid);
return result;
}
std::wstring xmlEscape(const std::wstring& value) {
std::wstring result;
result.reserve(value.size() + 16);
for (wchar_t ch : value) {
switch (ch) {
case L'&': result += L"&amp;"; break;
case L'<': result += L"&lt;"; break;
case L'>': result += L"&gt;"; break;
case L'\"': result += L"&quot;"; break;
case L'\'': result += L"&apos;"; break;
default: result.push_back(ch); break;
}
}
return result;
}
bool writeUtf16File(const std::wstring& path, const std::wstring& text,
std::wstring* error) {
HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY, nullptr);
if (file == INVALID_HANDLE_VALUE) {
if (error) *error = win32Error(GetLastError());
return false;
}
const WORD bom = 0xFEFF;
DWORD written = 0;
bool ok = WriteFile(file, &bom, sizeof(bom), &written, nullptr) != FALSE &&
written == sizeof(bom);
if (ok) {
const DWORD byteCount = static_cast<DWORD>(text.size() * sizeof(wchar_t));
written = 0;
ok = WriteFile(file, text.data(), byteCount, &written, nullptr) != FALSE &&
written == byteCount;
}
if (!ok && error) *error = win32Error(GetLastError());
CloseHandle(file);
return ok;
}
bool deleteTask(const wchar_t* taskName) {
const std::wstring command = quote(schtasksPath()) +
L" /Delete /F /TN " + quote(taskName);
const DWORD result = runHiddenProcess(command);
return result == ERROR_SUCCESS || result == 1;
}
bool createSilentTask(std::wstring* error) {
const std::wstring exe = executablePath();
const std::wstring sid = currentUserSid();
if (exe.empty() || sid.empty()) {
if (error) *error = L"Не удалось определить путь приложения или SID пользователя.";
return false;
}
wchar_t tempDirectory[MAX_PATH + 1]{};
wchar_t tempFile[MAX_PATH + 1]{};
if (GetTempPathW(MAX_PATH, tempDirectory) == 0 ||
GetTempFileNameW(tempDirectory, L"NSH", 0, tempFile) == 0) {
if (error) *error = win32Error(GetLastError());
return false;
}
const std::wstring xml =
L"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\r\n"
L"<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\r\n"
L" <RegistrationInfo><Description>NightShot silent elevated startup</Description></RegistrationInfo>\r\n"
L" <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>" + xmlEscape(sid) +
L"</UserId></LogonTrigger></Triggers>\r\n"
L" <Principals><Principal id=\"Author\"><UserId>" + xmlEscape(sid) +
L"</UserId><LogonType>InteractiveToken</LogonType><RunLevel>HighestAvailable</RunLevel>"
L"</Principal></Principals>\r\n"
L" <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>"
L"<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"
L"<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>"
L"<AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable>"
L"<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>"
L"<IdleSettings><StopOnIdleEnd>false</StopOnIdleEnd><RestartOnIdle>false</RestartOnIdle>"
L"</IdleSettings><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled>"
L"<Hidden>false</Hidden><RunOnlyIfIdle>false</RunOnlyIfIdle>"
L"<WakeToRun>false</WakeToRun><ExecutionTimeLimit>PT0S</ExecutionTimeLimit>"
L"<Priority>7</Priority></Settings>\r\n"
L" <Actions Context=\"Author\"><Exec><Command>" + xmlEscape(exe) +
L"</Command><Arguments>--startup --elevated</Arguments><WorkingDirectory>" +
xmlEscape(parentDirectory(exe)) +
L"</WorkingDirectory></Exec></Actions>\r\n"
L"</Task>\r\n";
bool ok = writeUtf16File(tempFile, xml, error);
if (ok) {
const std::wstring command = quote(schtasksPath()) + L" /Create /F /TN " +
quote(kTaskName) + L" /XML " + quote(tempFile);
const DWORD result = runHiddenProcess(command);
ok = result == ERROR_SUCCESS;
if (!ok && error) {
*error = L"Планировщик заданий вернул код " + std::to_wstring(result) + L".";
}
}
DeleteFileW(tempFile);
return ok;
}
} // namespace
bool isProcessElevated() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return false;
TOKEN_ELEVATION elevation{};
DWORD bytes = 0;
const BOOL ok = GetTokenInformation(token, TokenElevation, &elevation,
sizeof(elevation), &bytes);
CloseHandle(token);
return ok && elevation.TokenIsElevated != 0;
}
bool ensureElevated(const wchar_t* mainWindowClass) {
if ((mainWindowClass && FindWindowW(mainWindowClass, nullptr)) ||
FindWindowW(kLegacyMainWindowClass, nullptr)) {
return false;
}
if (isProcessElevated()) return true;
const std::vector<std::wstring> args = commandLineArguments();
if (std::find(args.begin(), args.end(), L"--elevated") != args.end()) {
MessageBoxW(nullptr, L"NightShot не получил права администратора.",
L"NightShot", MB_OK | MB_ICONERROR);
return false;
}
const std::wstring exe = executablePath();
const std::wstring parameters = elevationArguments();
if (exe.empty()) return false;
SHELLEXECUTEINFOW execution{};
execution.cbSize = sizeof(execution);
execution.fMask = SEE_MASK_FLAG_NO_UI;
execution.lpVerb = L"runas";
execution.lpFile = exe.c_str();
execution.lpParameters = parameters.c_str();
execution.nShow = SW_SHOWNORMAL;
if (!ShellExecuteExW(&execution)) {
const DWORD code = GetLastError();
if (code != ERROR_CANCELLED) {
const std::wstring message = L"Не удалось запустить NightShot от администратора:\n" +
win32Error(code);
MessageBoxW(nullptr, message.c_str(), L"NightShot", MB_OK | MB_ICONERROR);
}
}
return false;
}
bool applyAutostart(bool enabled, bool silentAdmin, std::wstring* error) {
if (error) error->clear();
// Remove stale autostart entries left by versions published as SnapLight.
// Failure here is non-fatal because the new entry/task is still authoritative.
deleteRunValue(kLegacyRunValue);
deleteTask(kLegacyTaskName);
if (!enabled) {
const bool runRemoved = setRunValue(false, error);
const bool taskRemoved = deleteTask(kTaskName);
if (!taskRemoved && error && error->empty()) {
*error = L"Не удалось удалить задачу Silent Admin.";
}
return runRemoved && taskRemoved;
}
if (silentAdmin) {
if (!setRunValue(false, error)) return false;
deleteTask(kTaskName);
return createSilentTask(error);
}
deleteTask(kTaskName);
return setRunValue(true, error);
}
} // namespace startup
+350
View File
@@ -0,0 +1,350 @@
#include "uploader.hpp"
#include "app_messages.hpp"
#include <winhttp.h>
#include <windows.h>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cwctype>
#include <limits>
#include <memory>
#include <string>
#include <vector>
namespace uploader {
namespace {
struct InternetHandle {
HINTERNET value = nullptr;
InternetHandle() = default;
explicit InternetHandle(HINTERNET handle) : value(handle) {}
~InternetHandle() { if (value) WinHttpCloseHandle(value); }
InternetHandle(const InternetHandle&) = delete;
InternetHandle& operator=(const InternetHandle&) = delete;
};
struct Job {
HWND target = nullptr;
std::vector<BYTE> png;
settings::AppSettings configuration;
};
std::string wideToUtf8(const std::wstring& value) {
if (value.empty()) return {};
const int bytes = WideCharToMultiByte(CP_UTF8, 0, value.c_str(),
static_cast<int>(value.size()),
nullptr, 0, nullptr, nullptr);
if (bytes <= 0) return {};
std::string result(static_cast<std::size_t>(bytes), '\0');
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()),
result.data(), bytes, nullptr, nullptr);
return result;
}
std::wstring utf8ToWide(const std::string& value) {
if (value.empty()) return {};
int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
value.data(), static_cast<int>(value.size()),
nullptr, 0);
UINT codePage = CP_UTF8;
DWORD flags = MB_ERR_INVALID_CHARS;
if (chars <= 0) {
codePage = CP_ACP;
flags = 0;
chars = MultiByteToWideChar(codePage, flags, value.data(),
static_cast<int>(value.size()), nullptr, 0);
}
if (chars <= 0) return {};
std::wstring result(static_cast<std::size_t>(chars), L'\0');
MultiByteToWideChar(codePage, flags, value.data(), static_cast<int>(value.size()),
result.data(), chars);
return result;
}
std::wstring trim(std::wstring value) {
while (!value.empty() && std::iswspace(value.front())) value.erase(value.begin());
while (!value.empty() && std::iswspace(value.back())) value.pop_back();
return value;
}
std::wstring win32Error(DWORD code) {
wchar_t* raw = nullptr;
const DWORD length = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, code, 0, reinterpret_cast<wchar_t*>(&raw), 0, nullptr);
std::wstring result = length && raw
? std::wstring(raw, length)
: L"Ошибка Windows " + std::to_wstring(code);
if (raw) LocalFree(raw);
return trim(std::move(result));
}
std::wstring percentEncode(const std::wstring& value) {
static constexpr wchar_t kHex[] = L"0123456789ABCDEF";
const std::string utf8 = wideToUtf8(value);
std::wstring result;
result.reserve(utf8.size() * 3);
for (unsigned char byte : utf8) {
if ((byte >= 'a' && byte <= 'z') ||
(byte >= 'A' && byte <= 'Z') ||
(byte >= '0' && byte <= '9') ||
byte == '-' || byte == '_' || byte == '.' || byte == '~') {
result.push_back(static_cast<wchar_t>(byte));
} else {
result.push_back(L'%');
result.push_back(kHex[(byte >> 4) & 0x0F]);
result.push_back(kHex[byte & 0x0F]);
}
}
return result;
}
std::string safeDispositionValue(const std::wstring& value) {
std::string result = wideToUtf8(value);
for (char& ch : result) {
if (ch == '\"' || ch == '\r' || ch == '\n' || ch == '\\') ch = '_';
}
return result;
}
void appendBytes(std::vector<BYTE>& output, const std::string& value) {
output.insert(output.end(), value.begin(), value.end());
}
std::wstring extractJsonString(const std::wstring& response,
const std::wstring& key) {
const std::wstring marker = L"\"" + key + L"\"";
std::size_t position = response.find(marker);
if (position == std::wstring::npos) return {};
position = response.find(L':', position + marker.size());
if (position == std::wstring::npos) return {};
position = response.find(L'\"', position + 1);
if (position == std::wstring::npos) return {};
++position;
std::wstring result;
bool escaped = false;
for (; position < response.size(); ++position) {
const wchar_t ch = response[position];
if (escaped) {
switch (ch) {
case L'n': result.push_back(L'\n'); break;
case L'r': result.push_back(L'\r'); break;
case L't': result.push_back(L'\t'); break;
case L'/': result.push_back(L'/'); break;
case L'\\': result.push_back(L'\\'); break;
case L'\"': result.push_back(L'\"'); break;
default: result.push_back(ch); break;
}
escaped = false;
} else if (ch == L'\\') {
escaped = true;
} else if (ch == L'\"') {
return result;
} else {
result.push_back(ch);
}
}
return {};
}
std::wstring responseUrl(const std::wstring& raw) {
const std::wstring response = trim(raw);
if (response.rfind(L"https://", 0) == 0 || response.rfind(L"http://", 0) == 0) {
const std::size_t end = response.find_first_of(L"\r\n\t ");
return response.substr(0, end);
}
for (const wchar_t* key : {L"url", L"link", L"location"}) {
std::wstring value = trim(extractJsonString(response, key));
if (value.rfind(L"https://", 0) == 0 || value.rfind(L"http://", 0) == 0) {
return value;
}
}
return {};
}
std::unique_ptr<Result> performUpload(const Job& job) {
auto result = std::make_unique<Result>();
if (job.png.empty()) {
result->message = L"PNG для загрузки пуст.";
return result;
}
std::wstring endpoint = settings::normalizeEndpoint(job.configuration.uploadUrl);
if (endpoint.empty()) {
result->message = L"URL API-сервера не настроен.";
return result;
}
const wchar_t separator = endpoint.find(L'?') == std::wstring::npos ? L'?' :
((endpoint.back() == L'?' || endpoint.back() == L'&') ? L'\0' : L'&');
if (separator != L'\0') endpoint.push_back(separator);
endpoint += L"name=" + percentEncode(job.configuration.uploadName);
URL_COMPONENTSW parts{};
parts.dwStructSize = sizeof(parts);
parts.dwSchemeLength = static_cast<DWORD>(-1);
parts.dwHostNameLength = static_cast<DWORD>(-1);
parts.dwUrlPathLength = static_cast<DWORD>(-1);
parts.dwExtraInfoLength = static_cast<DWORD>(-1);
if (!WinHttpCrackUrl(endpoint.c_str(), static_cast<DWORD>(endpoint.size()),
0, &parts) || parts.dwHostNameLength == 0) {
result->message = L"Не удалось разобрать URL API-сервера.";
return result;
}
const std::wstring host(parts.lpszHostName, parts.dwHostNameLength);
std::wstring path;
if (parts.dwUrlPathLength > 0) {
path.assign(parts.lpszUrlPath, parts.dwUrlPathLength);
}
if (path.empty()) path = L"/";
if (parts.dwExtraInfoLength > 0) {
path.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
}
InternetHandle session(WinHttpOpen(
L"NightShot/0.17", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
if (!session.value) {
result->message = L"WinHttpOpen: " + win32Error(GetLastError());
return result;
}
WinHttpSetTimeouts(session.value, 10000, 10000, 30000, 30000);
InternetHandle connection(WinHttpConnect(session.value, host.c_str(),
parts.nPort, 0));
if (!connection.value) {
result->message = L"WinHttpConnect: " + win32Error(GetLastError());
return result;
}
const DWORD requestFlags = parts.nScheme == INTERNET_SCHEME_HTTPS
? WINHTTP_FLAG_SECURE : 0;
InternetHandle request(WinHttpOpenRequest(
connection.value, L"POST", path.c_str(), nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, requestFlags));
if (!request.value) {
result->message = L"WinHttpOpenRequest: " + win32Error(GetLastError());
return result;
}
const std::string boundary = "----NightShotBoundary" +
std::to_string(GetCurrentProcessId()) + std::to_string(GetTickCount64());
const std::string field = safeDispositionValue(job.configuration.multipartField);
const std::string filename = safeDispositionValue(job.configuration.uploadName) + ".png";
std::vector<BYTE> body;
body.reserve(job.png.size() + 512);
appendBytes(body, "--" + boundary + "\r\n");
appendBytes(body, "Content-Disposition: form-data; name=\"" + field +
"\"; filename=\"" + filename + "\"\r\n");
appendBytes(body, "Content-Type: image/png\r\n\r\n");
body.insert(body.end(), job.png.begin(), job.png.end());
appendBytes(body, "\r\n--" + boundary + "--\r\n");
if (body.size() > std::numeric_limits<DWORD>::max()) {
result->message = L"Снимок слишком большой для WinHTTP.";
return result;
}
const std::wstring headers = L"Content-Type: multipart/form-data; boundary=" +
utf8ToWide(boundary) + L"\r\n";
if (!WinHttpSendRequest(
request.value, headers.c_str(), static_cast<DWORD>(headers.size()),
body.data(), static_cast<DWORD>(body.size()),
static_cast<DWORD>(body.size()), 0)) {
result->message = L"WinHttpSendRequest: " + win32Error(GetLastError());
return result;
}
if (!WinHttpReceiveResponse(request.value, nullptr)) {
result->message = L"WinHttpReceiveResponse: " + win32Error(GetLastError());
return result;
}
DWORD status = 0;
DWORD statusBytes = sizeof(status);
WinHttpQueryHeaders(request.value,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusBytes,
WINHTTP_NO_HEADER_INDEX);
result->httpStatus = status;
std::string responseBytes;
constexpr std::size_t kResponseLimit = 4u * 1024u * 1024u;
for (;;) {
DWORD available = 0;
if (!WinHttpQueryDataAvailable(request.value, &available)) {
result->message = L"WinHttpQueryDataAvailable: " + win32Error(GetLastError());
return result;
}
if (available == 0) break;
if (responseBytes.size() + available > kResponseLimit) {
result->message = L"Ответ API-сервера слишком большой.";
return result;
}
const std::size_t oldSize = responseBytes.size();
responseBytes.resize(oldSize + available);
DWORD read = 0;
if (!WinHttpReadData(request.value, responseBytes.data() + oldSize,
available, &read)) {
result->message = L"WinHttpReadData: " + win32Error(GetLastError());
return result;
}
responseBytes.resize(oldSize + read);
if (read == 0) break;
}
const std::wstring response = trim(utf8ToWide(responseBytes));
if (status < 200 || status >= 300) {
result->message = L"API-сервер вернул HTTP " + std::to_wstring(status);
if (!response.empty()) result->message += L":\n" + response.substr(0, 1000);
return result;
}
result->success = true;
result->responseUrl = responseUrl(response);
result->message = result->responseUrl.empty()
? (response.empty() ? L"Снимок загружен." : response.substr(0, 1000))
: L"Снимок загружен.";
return result;
}
DWORD WINAPI workerProc(void* raw) {
std::unique_ptr<Job> job(static_cast<Job*>(raw));
std::unique_ptr<Result> result = performUpload(*job);
Result* transferred = result.release();
if (!job->target || !IsWindow(job->target) ||
!PostMessageW(job->target, WM_NIGHTSHOT_UPLOAD_FINISHED,
0, reinterpret_cast<LPARAM>(transferred))) {
delete transferred;
}
return 0;
}
} // namespace
bool start(HWND notificationTarget, std::vector<BYTE>&& png,
const settings::AppSettings& configuration) {
if (!notificationTarget || !IsWindow(notificationTarget) || png.empty()) return false;
auto* job = new Job();
job->target = notificationTarget;
job->png = std::move(png);
job->configuration = configuration;
HANDLE thread = CreateThread(nullptr, 0, workerProc, job, 0, nullptr);
if (!thread) {
delete job;
return false;
}
CloseHandle(thread);
return true;
}
} // namespace uploader