#include "stdafx.h" /** * @file TetrisLayout.cpp * @brief 实现窗口缩放布局和各类按钮、卡片、列表项的点击区域计算。 */ #include "TetrisAppInternal.h" /** * @brief 将指定滚动偏移按步长调整,并限制在非负范围内。 * @param scrollOffset 需要修改的滚动偏移。 * @param delta 本次滚动增量。 */ void AdjustScrollOffset(int& scrollOffset, int delta) { // 先应用本次滚动增量,再统一夹紧到允许范围内。 scrollOffset += delta; if (scrollOffset < 0) { scrollOffset = 0; } if (scrollOffset > 2400) { scrollOffset = 2400; } } /** * @brief 按当前窗口缩放返回一次滚动操作的像素距离。 * @param hWnd 当前窗口句柄。 * @param baseStep 设计稿中的基础滚动步长。 * @return 缩放后的滚动步长。 */ int GetScrollStep(HWND hWnd, int baseStep) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); return MulDiv(baseStep, metrics.scale, 1000); } /** * @brief 根据当前窗口大小计算整体界面缩放与偏移。 * @param hWnd 当前窗口句柄。 * @return 布局缩放、偏移和网格尺寸。 */ LayoutMetrics GetLayoutMetrics(HWND hWnd) { RECT clientRect; GetClientRect(hWnd, &clientRect); // 以设计稿窗口为基准计算缩放,取较小比例保证完整界面不被裁切。 int clientWidth = clientRect.right - clientRect.left; int clientHeight = clientRect.bottom - clientRect.top; int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH); int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT); int scale = (scaleX < scaleY) ? scaleX : scaleY; if (scale < 500) { scale = 500; } LayoutMetrics metrics = {}; metrics.scale = scale; metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000); metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000); // 横向居中显示,纵向从顶部开始,方便窗口高度不足时保持棋盘起点稳定。 metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2; metrics.offsetY = 0; metrics.grid = MulDiv(GRID, scale, 1000); return metrics; } /** * @brief 按当前布局比例缩放一个尺寸值。 * @param metrics 当前布局参数。 * @param value 设计稿尺寸值。 * @return 缩放后的像素尺寸。 */ int ScaleValue(const LayoutMetrics& metrics, int value) { return MulDiv(value, metrics.scale, 1000); } /** * @brief 按当前布局比例缩放横坐标并叠加窗口偏移。 * @param metrics 当前布局参数。 * @param value 设计稿横坐标。 * @return 实际窗口横坐标。 */ int ScaleXValue(const LayoutMetrics& metrics, int value) { return metrics.offsetX + MulDiv(value, metrics.scale, 1000); } /** * @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。 * @param metrics 当前布局参数。 * @param value 设计稿纵坐标。 * @return 实际窗口纵坐标。 */ int ScaleYValue(const LayoutMetrics& metrics, int value) { return metrics.offsetY + MulDiv(value, metrics.scale, 1000); } /** * @brief 获取主菜单中央卡片区域。 * @param hWnd 当前窗口句柄。 * @return 菜单卡片在窗口中的矩形区域。 */ static RECT GetMenuCardRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rect = { ScaleXValue(metrics, 110), ScaleYValue(metrics, 70), ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 110), ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 70) }; return rect; } /** * @brief 获取主菜单选项的点击区域。 * @param hWnd 当前窗口句柄。 * @param index 菜单选项序号。 * @return 选项在窗口中的矩形区域。 */ RECT GetMenuOptionRect(HWND hWnd, int index) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT menuCard = GetMenuCardRect(hWnd); int top = menuCard.top + ScaleValue(metrics, 140) + index * ScaleValue(metrics, 130); RECT rect = { menuCard.left + ScaleValue(metrics, 36), top, menuCard.right - ScaleValue(metrics, 36), top + ScaleValue(metrics, 104) }; return rect; } /** * @brief 获取帮助页卡片区域。 * @param hWnd 当前窗口句柄。 * @return 帮助卡片在窗口中的矩形区域。 */ static RECT GetRulesCardRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rect = { ScaleXValue(metrics, 76), ScaleYValue(metrics, 54), ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 76), ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 54) }; return rect; } /** * @brief 获取帮助页首页选项的点击区域。 * @param hWnd 当前窗口句柄。 * @param index 帮助选项序号。 * @return 选项在窗口中的矩形区域。 */ RECT GetHelpOptionRect(HWND hWnd, int index) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rulesCard = GetRulesCardRect(hWnd); RECT contentRect = { rulesCard.left + ScaleValue(metrics, 36), rulesCard.top + ScaleValue(metrics, 126), rulesCard.right - ScaleValue(metrics, 36), rulesCard.bottom - ScaleValue(metrics, 86) }; int optionHeight = ScaleValue(metrics, 100); int optionGap = ScaleValue(metrics, 22); int optionTop = contentRect.top + ScaleValue(metrics, 18); RECT rect = { contentRect.left, optionTop + index * (optionHeight + optionGap), contentRect.right, optionTop + index * (optionHeight + optionGap) + optionHeight }; return rect; } /** * @brief 获取技能演示列表项的点击区域。 * @param hWnd 当前窗口句柄。 * @param index 技能演示条目序号。 * @return 条目在窗口中的矩形区域。 */ RECT GetHelpSkillDemoItemRect(HWND hWnd, int index) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rulesCard = GetRulesCardRect(hWnd); RECT contentRect = { rulesCard.left + ScaleValue(metrics, 36), rulesCard.top + ScaleValue(metrics, 126), rulesCard.right - ScaleValue(metrics, 36), rulesCard.bottom - ScaleValue(metrics, 86) }; int itemHeight = ScaleValue(metrics, 58); int itemGap = ScaleValue(metrics, 10); int itemTop = contentRect.top + ScaleValue(metrics, 8) - helpScrollOffset; RECT rect = { contentRect.left, itemTop + index * (itemHeight + itemGap), contentRect.right, itemTop + index * (itemHeight + itemGap) + itemHeight }; return rect; } /** * @brief 获取帮助页底部返回提示的点击区域。 * @param hWnd 当前窗口句柄。 * @return 返回提示在窗口中的矩形区域。 */ RECT GetHelpBackHintRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rulesCard = GetRulesCardRect(hWnd); RECT rect = { rulesCard.left + ScaleValue(metrics, 36), rulesCard.bottom - ScaleValue(metrics, 58), rulesCard.right - ScaleValue(metrics, 36), rulesCard.bottom - ScaleValue(metrics, 24) }; return rect; } /** * @brief 获取致谢页左右箭头按钮区域。 * @param hWnd 当前窗口句柄。 * @param direction 小于 0 为左箭头,大于 0 为右箭头。 * @return 箭头按钮在窗口中的矩形区域。 */ RECT GetCreditArrowRect(HWND hWnd, int direction) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rulesCard = GetRulesCardRect(hWnd); int size = ScaleValue(metrics, 54); int centerY = (rulesCard.top + rulesCard.bottom) / 2; int left = direction < 0 ? rulesCard.left + ScaleValue(metrics, 52) : rulesCard.right - ScaleValue(metrics, 52) - size; RECT rect = { left, centerY - size / 2, left + size, centerY + size / 2 }; return rect; } /** * @brief 获取升级选择覆盖层区域。 * @param hWnd 当前窗口句柄。 * @return 覆盖层在窗口中的矩形区域。 */ static RECT GetUpgradeOverlayRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rect = { ScaleXValue(metrics, 60), ScaleYValue(metrics, 80), ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 60), ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 80) }; return rect; } /** * @brief 获取升级选择卡片的点击区域。 * @param hWnd 当前窗口句柄。 * @param index 强化卡片序号。 * @return 卡片在窗口中的矩形区域。 */ RECT GetUpgradeCardRect(HWND hWnd, int index) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT overlayRect = GetUpgradeOverlayRect(hWnd); // 根据当前候选数量自动决定列数;最多三列,两行用于命运轮盘六选项。 int gap = ScaleValue(metrics, 18); int horizontalPadding = ScaleValue(metrics, 36); int verticalTop = overlayRect.top + ScaleValue(metrics, 138); int columnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3; if (columnCount < 1) { columnCount = 1; } int rowCount = (upgradeUiState.optionCount + columnCount - 1) / columnCount; if (rowCount < 1) { rowCount = 1; } int cardWidth = (overlayRect.right - overlayRect.left - horizontalPadding * 2 - gap * (columnCount - 1)) / columnCount; int availableHeight = overlayRect.bottom - verticalTop - ScaleValue(metrics, 72) - (rowCount - 1) * gap; int cardHeight = availableHeight / rowCount; int column = index % columnCount; int row = index / columnCount; int left = overlayRect.left + horizontalPadding + column * (cardWidth + gap); int top = verticalTop + row * (cardHeight + gap); RECT rect = { left, top, left + cardWidth, top + cardHeight }; return rect; } /** * @brief 获取暂停或结束提示覆盖层区域。 * @param hWnd 当前窗口句柄。 * @return 覆盖层在窗口中的矩形区域。 */ static RECT GetGameOverlayRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); int panelGap = ScaleValue(metrics, SIDE_PANEL_GAP); int panelWidth = ScaleValue(metrics, SIDE_PANEL_WIDTH); int boardLeft = ScaleXValue(metrics, WINDOW_PADDING) + panelWidth + panelGap; int boardTop = ScaleYValue(metrics, WINDOW_PADDING); int boardWidth = nGameWidth * metrics.grid; RECT rect = { boardLeft + ScaleValue(metrics, 28), boardTop + metrics.grid * 6 + ScaleValue(metrics, 10), boardLeft + boardWidth - ScaleValue(metrics, 28), boardTop + metrics.grid * 10 + ScaleValue(metrics, 30) }; return rect; } /** * @brief 获取暂停或结束覆盖层按钮的点击区域。 * @param hWnd 当前窗口句柄。 * @param index 按钮序号。 * @param buttonCount 当前覆盖层按钮总数。 * @return 按钮在窗口中的矩形区域。 */ RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT overlayRect = GetGameOverlayRect(hWnd); // 游戏结束可能有三个按钮,暂停只有两个按钮,因此间距和边距分开计算。 int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18); int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34); int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount; int height = ScaleValue(metrics, 44); int left = overlayRect.left + sidePadding + index * (width + gap); int top = overlayRect.top + ScaleValue(metrics, 94); RECT rect = { left, top, left + width, top + height }; return rect; } /** * @brief 获取左上角返回按钮的点击区域。 * @param hWnd 当前窗口句柄。 * @return 返回按钮在窗口中的矩形区域。 */ RECT GetBackButtonRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT rect = { ScaleXValue(metrics, 6), ScaleYValue(metrics, 6), ScaleXValue(metrics, 34), ScaleYValue(metrics, 34) }; return rect; } /** * @brief 获取右下角音乐按钮的点击区域。 * @param hWnd 当前窗口句柄。 * @return 音乐按钮在窗口中的矩形区域。 */ RECT GetMusicButtonRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); // 音乐按钮保持最小可点击尺寸,避免窗口缩小时变得难以点中。 int size = ScaleValue(metrics, 28); if (size < 22) { size = 22; } int marginRight = ScaleValue(metrics, 12); if (marginRight < 6) { marginRight = 6; } int marginBottom = ScaleValue(metrics, 12); if (marginBottom < 6) { marginBottom = 6; } RECT buttonRect = { metrics.offsetX + metrics.layoutWidth - marginRight - size, metrics.offsetY + metrics.layoutHeight - marginBottom - size, metrics.offsetX + metrics.layoutWidth - marginRight, metrics.offsetY + metrics.layoutHeight - marginBottom }; return buttonRect; } /** * @brief 判断点坐标是否落在矩形内部。 * @param rect 待判断矩形。 * @param x 点的横坐标。 * @param y 点的纵坐标。 * @return 点在矩形内返回 true,否则返回 false。 */ bool IsPointInRect(const RECT& rect, int x, int y) { return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; }