5.1 窗口系统概述
Sciter窗口架构
Sciter提供了强大的窗口管理系统,支持创建和管理多种类型的窗口。每个Sciter窗口都是一个独立的渲染上下文,可以包含完整的HTML/CSS/TIScript内容。
窗口类型
- 主窗口:应用程序的主要界面窗口
- 对话框:模态或非模态的对话框窗口
- 弹出窗口:临时显示的弹出式窗口
- 工具窗口:辅助工具和面板窗口
- 子窗口:嵌入在其他窗口中的子窗口
窗口特性
- 独立渲染:每个窗口有独立的DOM和渲染上下文
- 事件隔离:窗口间事件相互独立
- 资源共享:可以共享样式表和脚本资源
- 通信机制:支持窗口间消息传递
- 生命周期管理:完整的窗口创建、显示、隐藏、销毁流程
5.2 窗口创建与管理
基本窗口操作
1. 创建窗口
// 窗口管理器类
class WindowManager {
function this() {
this.windows = new Map();
this.windowCounter = 0;
}
// 创建新窗口
function createWindow(options = {}) {
var windowId = "window_" + (++this.windowCounter);
var defaultOptions = {
url: "about:blank",
width: 800,
height: 600,
x: undefined,
y: undefined,
resizable: true,
minimizable: true,
maximizable: true,
closable: true,
modal: false,
topmost: false,
title: "Sciter Window",
icon: undefined,
parent: undefined
};
var config = Object.assign(defaultOptions, options);
try {
// 使用Sciter API创建窗口
var window = view.window(config.url, {
x: config.x,
y: config.y,
width: config.width,
height: config.height,
resizable: config.resizable,
minimizable: config.minimizable,
maximizable: config.maximizable,
closable: config.closable,
modal: config.modal,
topmost: config.topmost,
title: config.title,
icon: config.icon,
parent: config.parent
});
// 存储窗口引用
this.windows.set(windowId, {
id: windowId,
window: window,
config: config,
created: Date.now(),
state: "created"
});
// 设置窗口事件处理
this.setupWindowEvents(windowId, window);
stdout.println(`窗口创建成功: ${windowId}`);
return windowId;
} catch (error) {
stderr.println(`窗口创建失败: ${error.message}`);
return null;
}
}
// 设置窗口事件
function setupWindowEvents(windowId, window) {
var self = this;
// 窗口关闭事件
window.on("close", function() {
self.handleWindowClose(windowId);
});
// 窗口激活事件
window.on("activate", function() {
self.handleWindowActivate(windowId);
});
// 窗口大小改变事件
window.on("size", function(width, height) {
self.handleWindowResize(windowId, width, height);
});
// 窗口移动事件
window.on("move", function(x, y) {
self.handleWindowMove(windowId, x, y);
});
}
// 显示窗口
function showWindow(windowId, state = "normal") {
var windowInfo = this.windows.get(windowId);
if (!windowInfo) {
stderr.println(`窗口不存在: ${windowId}`);
return false;
}
try {
switch (state) {
case "normal":
windowInfo.window.show();
break;
case "minimized":
windowInfo.window.minimize();
break;
case "maximized":
windowInfo.window.maximize();
break;
case "fullscreen":
windowInfo.window.fullscreen();
break;
}
windowInfo.state = state;
stdout.println(`窗口显示: ${windowId} (${state})`);
return true;
} catch (error) {
stderr.println(`窗口显示失败: ${error.message}`);
return false;
}
}
// 隐藏窗口
function hideWindow(windowId) {
var windowInfo = this.windows.get(windowId);
if (!windowInfo) return false;
try {
windowInfo.window.hide();
windowInfo.state = "hidden";
stdout.println(`窗口隐藏: ${windowId}`);
return true;
} catch (error) {
stderr.println(`窗口隐藏失败: ${error.message}`);
return false;
}
}
// 关闭窗口
function closeWindow(windowId) {
var windowInfo = this.windows.get(windowId);
if (!windowInfo) return false;
try {
windowInfo.window.close();
this.windows.delete(windowId);
stdout.println(`窗口关闭: ${windowId}`);
return true;
} catch (error) {
stderr.println(`窗口关闭失败: ${error.message}`);
return false;
}
}
// 获取窗口信息
function getWindowInfo(windowId) {
return this.windows.get(windowId);
}
// 获取所有窗口
function getAllWindows() {
return Array.from(this.windows.values());
}
// 查找窗口
function findWindow(predicate) {
for (var windowInfo of this.windows.values()) {
if (predicate(windowInfo)) {
return windowInfo;
}
}
return null;
}
// 窗口事件处理器
function handleWindowClose(windowId) {
stdout.println(`窗口关闭事件: ${windowId}`);
this.windows.delete(windowId);
// 如果是最后一个窗口,退出应用
if (this.windows.size === 0) {
view.quit();
}
}
function handleWindowActivate(windowId) {
stdout.println(`窗口激活: ${windowId}`);
var windowInfo = this.windows.get(windowId);
if (windowInfo) {
windowInfo.lastActivated = Date.now();
}
}
function handleWindowResize(windowId, width, height) {
stdout.println(`窗口大小改变: ${windowId} (${width}x${height})`);
var windowInfo = this.windows.get(windowId);
if (windowInfo) {
windowInfo.config.width = width;
windowInfo.config.height = height;
}
}
function handleWindowMove(windowId, x, y) {
stdout.println(`窗口移动: ${windowId} (${x}, ${y})`);
var windowInfo = this.windows.get(windowId);
if (windowInfo) {
windowInfo.config.x = x;
windowInfo.config.y = y;
}
}
}
// 创建全局窗口管理器
var windowManager = new WindowManager();
// 使用示例
function createMainWindow() {
var mainWindowId = windowManager.createWindow({
url: "main.html",
title: "主窗口",
width: 1200,
height: 800,
x: 100,
y: 100
});
windowManager.showWindow(mainWindowId);
return mainWindowId;
}
function createSettingsDialog() {
var dialogId = windowManager.createWindow({
url: "settings.html",
title: "设置",
width: 600,
height: 400,
modal: true,
resizable: false,
parent: windowManager.findWindow(w => w.config.title === "主窗口")
});
windowManager.showWindow(dialogId);
return dialogId;
}
2. 窗口通信
// 窗口间通信管理器
class WindowCommunication {
function this() {
this.messageHandlers = new Map();
this.setupGlobalMessageHandler();
}
// 设置全局消息处理
function setupGlobalMessageHandler() {
var self = this;
// 监听窗口消息
view.on("message", function(evt) {
self.handleMessage(evt);
});
}
// 发送消息到指定窗口
function sendMessage(targetWindowId, messageType, data) {
var targetWindow = windowManager.getWindowInfo(targetWindowId);
if (!targetWindow) {
stderr.println(`目标窗口不存在: ${targetWindowId}`);
return false;
}
var message = {
type: messageType,
data: data,
sender: view.windowId || "unknown",
timestamp: Date.now()
};
try {
targetWindow.window.postMessage(message);
stdout.println(`消息发送成功: ${messageType} -> ${targetWindowId}`);
return true;
} catch (error) {
stderr.println(`消息发送失败: ${error.message}`);
return false;
}
}
// 广播消息到所有窗口
function broadcastMessage(messageType, data, excludeSelf = true) {
var currentWindowId = view.windowId;
var sentCount = 0;
for (var windowInfo of windowManager.getAllWindows()) {
if (excludeSelf && windowInfo.id === currentWindowId) {
continue;
}
if (this.sendMessage(windowInfo.id, messageType, data)) {
sentCount++;
}
}
stdout.println(`广播消息: ${messageType} (发送到 ${sentCount} 个窗口)`);
return sentCount;
}
// 注册消息处理器
function registerHandler(messageType, handler) {
if (!this.messageHandlers.has(messageType)) {
this.messageHandlers.set(messageType, []);
}
this.messageHandlers.get(messageType).push(handler);
stdout.println(`注册消息处理器: ${messageType}`);
}
// 取消注册消息处理器
function unregisterHandler(messageType, handler) {
if (!this.messageHandlers.has(messageType)) return;
var handlers = this.messageHandlers.get(messageType);
var index = handlers.indexOf(handler);
if (index >= 0) {
handlers.splice(index, 1);
stdout.println(`取消注册消息处理器: ${messageType}`);
}
}
// 处理接收到的消息
function handleMessage(evt) {
var message = evt.data;
if (!message || !message.type) return;
stdout.println(`接收消息: ${message.type} from ${message.sender}`);
var handlers = this.messageHandlers.get(message.type);
if (handlers) {
for (var handler of handlers) {
try {
handler(message.data, message);
} catch (error) {
stderr.println(`消息处理器错误: ${error.message}`);
}
}
}
}
// 请求-响应模式
function sendRequest(targetWindowId, requestType, data, timeout = 5000) {
return new Promise(function(resolve, reject) {
var requestId = "req_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
// 注册响应处理器
var responseHandler = function(responseData, message) {
if (message.requestId === requestId) {
windowComm.unregisterHandler(requestType + "_response", responseHandler);
resolve(responseData);
}
};
windowComm.registerHandler(requestType + "_response", responseHandler);
// 设置超时
setTimeout(function() {
windowComm.unregisterHandler(requestType + "_response", responseHandler);
reject(new Error("请求超时"));
}, timeout);
// 发送请求
var requestData = {
requestId: requestId,
data: data
};
if (!windowComm.sendMessage(targetWindowId, requestType, requestData)) {
reject(new Error("发送请求失败"));
}
});
}
// 响应请求
function sendResponse(targetWindowId, requestType, requestId, responseData) {
var response = {
requestId: requestId,
data: responseData
};
return this.sendMessage(targetWindowId, requestType + "_response", response);
}
}
// 创建全局窗口通信管理器
var windowComm = new WindowCommunication();
// 使用示例
function setupWindowCommunication() {
// 注册消息处理器
windowComm.registerHandler("user-data-update", function(data) {
stdout.println("用户数据更新: " + JSON.stringify(data));
updateUserInterface(data);
});
windowComm.registerHandler("settings-changed", function(data) {
stdout.println("设置改变: " + JSON.stringify(data));
applySettings(data);
});
windowComm.registerHandler("get-user-info", function(data, message) {
var userInfo = getCurrentUserInfo();
windowComm.sendResponse(message.sender, "get-user-info", message.requestId, userInfo);
});
}
// 发送消息示例
function notifyUserDataUpdate(userData) {
windowComm.broadcastMessage("user-data-update", userData);
}
function requestUserInfo(targetWindowId) {
windowComm.sendRequest(targetWindowId, "get-user-info", {})
.then(function(userInfo) {
stdout.println("获取用户信息成功: " + JSON.stringify(userInfo));
})
.catch(function(error) {
stderr.println("获取用户信息失败: " + error.message);
});
}
5.3 布局系统
CSS布局增强
1. Flow布局
/* Flow布局是Sciter特有的布局方式 */
.flow-container {
flow: horizontal; /* 水平流式布局 */
/* flow: vertical; 垂直流式布局 */
/* flow: horizontal-wrap; 水平换行布局 */
/* flow: vertical-wrap; 垂直换行布局 */
}
/* 水平布局示例 */
.horizontal-layout {
flow: horizontal;
width: 100%;
height: 60px;
border: 1px solid #ccc;
}
.horizontal-layout > .item {
width: 100px;
height: 50px;
margin: 5px;
background: #e0e0e0;
text-align: center;
line-height: 50px;
}
/* 垂直布局示例 */
.vertical-layout {
flow: vertical;
width: 200px;
height: 100%;
border: 1px solid #ccc;
}
.vertical-layout > .item {
width: 180px;
height: 40px;
margin: 5px 10px;
background: #f0f0f0;
text-align: center;
line-height: 40px;
}
/* 网格布局示例 */
.grid-layout {
flow: horizontal-wrap;
width: 100%;
padding: 10px;
}
.grid-layout > .grid-item {
width: 150px;
height: 100px;
margin: 5px;
background: #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
/* 弹性布局 */
.flex-container {
display: flex;
flex-direction: row; /* row, column, row-reverse, column-reverse */
justify-content: space-between; /* flex-start, flex-end, center, space-between, space-around */
align-items: center; /* flex-start, flex-end, center, stretch, baseline */
flex-wrap: wrap; /* nowrap, wrap, wrap-reverse */
gap: 10px;
}
.flex-item {
flex: 1; /* flex-grow flex-shrink flex-basis */
/* flex: 0 1 auto; 默认值 */
/* flex: 1 1 0; 等分空间 */
/* flex: 0 0 200px; 固定宽度 */
min-width: 100px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.responsive-container {
flow: vertical;
}
.responsive-container > .item {
width: 100%;
margin: 5px 0;
}
}
@media screen and (min-width: 769px) {
.responsive-container {
flow: horizontal;
}
.responsive-container > .item {
flex: 1;
margin: 0 5px;
}
}
2. 高级布局组件
// 布局管理器
class LayoutManager {
function this() {
this.layouts = new Map();
this.observers = new Map();
}
// 注册布局
function registerLayout(name, layoutConfig) {
this.layouts.set(name, layoutConfig);
stdout.println(`注册布局: ${name}`);
}
// 应用布局
function applyLayout(element, layoutName, options = {}) {
var layout = this.layouts.get(layoutName);
if (!layout) {
stderr.println(`布局不存在: ${layoutName}`);
return false;
}
try {
layout.apply(element, options);
// 设置响应式观察器
if (layout.responsive) {
this.setupResponsiveObserver(element, layout, options);
}
stdout.println(`应用布局成功: ${layoutName}`);
return true;
} catch (error) {
stderr.println(`应用布局失败: ${error.message}`);
return false;
}
}
// 设置响应式观察器
function setupResponsiveObserver(element, layout, options) {
var self = this;
var observer = new ResizeObserver(function(entries) {
for (var entry of entries) {
self.handleElementResize(entry.target, layout, options);
}
});
observer.observe(element);
this.observers.set(element, observer);
}
// 处理元素大小改变
function handleElementResize(element, layout, options) {
if (layout.onResize) {
layout.onResize(element, options);
}
}
// 移除布局观察器
function removeObserver(element) {
var observer = this.observers.get(element);
if (observer) {
observer.disconnect();
this.observers.delete(element);
}
}
}
// 创建布局管理器
var layoutManager = new LayoutManager();
// 注册常用布局
function registerCommonLayouts() {
// 卡片网格布局
layoutManager.registerLayout("card-grid", {
apply: function(element, options) {
var cardWidth = options.cardWidth || 200;
var gap = options.gap || 10;
element.style.display = "grid";
element.style.gridTemplateColumns = `repeat(auto-fill, minmax(${cardWidth}px, 1fr))`;
element.style.gap = gap + "px";
element.style.padding = gap + "px";
},
responsive: true,
onResize: function(element, options) {
// 响应式调整
var width = element.clientWidth;
var cardWidth = options.cardWidth || 200;
var gap = options.gap || 10;
if (width < 600) {
element.style.gridTemplateColumns = "1fr";
} else if (width < 900) {
element.style.gridTemplateColumns = "repeat(2, 1fr)";
} else {
element.style.gridTemplateColumns = `repeat(auto-fill, minmax(${cardWidth}px, 1fr))`;
}
}
});
// 侧边栏布局
layoutManager.registerLayout("sidebar", {
apply: function(element, options) {
var sidebarWidth = options.sidebarWidth || 250;
var position = options.position || "left";
element.style.display = "flex";
element.style.height = "100%";
var sidebar = element.$("[data-role='sidebar']");
var content = element.$("[data-role='content']");
if (sidebar && content) {
sidebar.style.width = sidebarWidth + "px";
sidebar.style.flexShrink = "0";
content.style.flex = "1";
content.style.overflow = "auto";
if (position === "right") {
element.style.flexDirection = "row-reverse";
} else {
element.style.flexDirection = "row";
}
}
},
responsive: true,
onResize: function(element, options) {
var width = element.clientWidth;
var sidebar = element.$("[data-role='sidebar']");
if (width < 768 && sidebar) {
// 移动端隐藏侧边栏
sidebar.style.display = "none";
} else if (sidebar) {
sidebar.style.display = "block";
}
}
});
// 头部-内容-底部布局
layoutManager.registerLayout("header-content-footer", {
apply: function(element, options) {
var headerHeight = options.headerHeight || 60;
var footerHeight = options.footerHeight || 40;
element.style.display = "flex";
element.style.flexDirection = "column";
element.style.height = "100%";
var header = element.$("[data-role='header']");
var content = element.$("[data-role='content']");
var footer = element.$("[data-role='footer']");
if (header) {
header.style.height = headerHeight + "px";
header.style.flexShrink = "0";
}
if (content) {
content.style.flex = "1";
content.style.overflow = "auto";
}
if (footer) {
footer.style.height = footerHeight + "px";
footer.style.flexShrink = "0";
}
}
});
// 瀑布流布局
layoutManager.registerLayout("masonry", {
apply: function(element, options) {
var columns = options.columns || 3;
var gap = options.gap || 10;
element.style.columnCount = columns;
element.style.columnGap = gap + "px";
element.style.columnFill = "balance";
// 为子元素设置样式
var items = element.$$("> *");
items.forEach(function(item) {
item.style.breakInside = "avoid";
item.style.marginBottom = gap + "px";
});
},
responsive: true,
onResize: function(element, options) {
var width = element.clientWidth;
var columns;
if (width < 600) {
columns = 1;
} else if (width < 900) {
columns = 2;
} else if (width < 1200) {
columns = 3;
} else {
columns = options.columns || 4;
}
element.style.columnCount = columns;
}
});
}
// 注册布局
registerCommonLayouts();
// 使用示例
function setupPageLayout() {
// 应用主布局
var mainContainer = $("#main-container");
layoutManager.applyLayout(mainContainer, "header-content-footer", {
headerHeight: 80,
footerHeight: 50
});
// 应用侧边栏布局
var contentArea = $("#content-area");
layoutManager.applyLayout(contentArea, "sidebar", {
sidebarWidth: 300,
position: "left"
});
// 应用卡片网格布局
var cardContainer = $("#card-container");
layoutManager.applyLayout(cardContainer, "card-grid", {
cardWidth: 250,
gap: 15
});
}
动态布局
1. 可调整大小的面板
// 可调整大小的分割面板
class ResizablePanel {
function this(container, options = {}) {
this.container = container;
this.options = Object.assign({
direction: "horizontal", // horizontal, vertical
minSize: 100,
maxSize: null,
initialSize: null,
resizable: true,
collapsible: false
}, options);
this.isResizing = false;
this.startPos = 0;
this.startSize = 0;
this.init();
}
function init() {
this.container.addClass("resizable-panel");
this.container.addClass(this.options.direction);
if (this.options.resizable) {
this.createResizeHandle();
}
if (this.options.collapsible) {
this.createCollapseButton();
}
this.setupEvents();
// 设置初始大小
if (this.options.initialSize) {
this.setSize(this.options.initialSize);
}
}
function createResizeHandle() {
this.resizeHandle = Element.create("div", {
class: "resize-handle " + this.options.direction
});
this.container.append(this.resizeHandle);
}
function createCollapseButton() {
this.collapseButton = Element.create("button", {
class: "collapse-button",
html: "◀"
});
this.container.append(this.collapseButton);
}
function setupEvents() {
var self = this;
if (this.resizeHandle) {
this.resizeHandle.on("mousedown", function(evt) {
self.startResize(evt);
});
}
if (this.collapseButton) {
this.collapseButton.on("click", function(evt) {
self.toggleCollapse();
});
}
document.on("mousemove", function(evt) {
if (self.isResizing) {
self.handleResize(evt);
}
});
document.on("mouseup", function(evt) {
if (self.isResizing) {
self.endResize(evt);
}
});
}
function startResize(evt) {
this.isResizing = true;
this.startPos = this.options.direction === "horizontal" ? evt.clientX : evt.clientY;
this.startSize = this.getCurrentSize();
document.body.addClass("resizing");
this.container.addClass("resizing");
evt.preventDefault();
}
function handleResize(evt) {
var currentPos = this.options.direction === "horizontal" ? evt.clientX : evt.clientY;
var delta = currentPos - this.startPos;
var newSize = this.startSize + delta;
// 应用大小限制
if (this.options.minSize && newSize < this.options.minSize) {
newSize = this.options.minSize;
}
if (this.options.maxSize && newSize > this.options.maxSize) {
newSize = this.options.maxSize;
}
this.setSize(newSize);
}
function endResize(evt) {
this.isResizing = false;
document.body.removeClass("resizing");
this.container.removeClass("resizing");
// 触发调整完成事件
this.container.dispatchEvent(new CustomEvent("resize-end", {
detail: { size: this.getCurrentSize() }
}));
}
function setSize(size) {
if (this.options.direction === "horizontal") {
this.container.style.width = size + "px";
} else {
this.container.style.height = size + "px";
}
// 触发调整事件
this.container.dispatchEvent(new CustomEvent("resize", {
detail: { size: size }
}));
}
function getCurrentSize() {
if (this.options.direction === "horizontal") {
return this.container.clientWidth;
} else {
return this.container.clientHeight;
}
}
function toggleCollapse() {
if (this.container.hasClass("collapsed")) {
this.expand();
} else {
this.collapse();
}
}
function collapse() {
this.savedSize = this.getCurrentSize();
this.container.addClass("collapsed");
this.setSize(0);
if (this.collapseButton) {
this.collapseButton.innerHTML = this.options.direction === "horizontal" ? "▶" : "▼";
}
}
function expand() {
this.container.removeClass("collapsed");
this.setSize(this.savedSize || this.options.initialSize || 200);
if (this.collapseButton) {
this.collapseButton.innerHTML = this.options.direction === "horizontal" ? "◀" : "▲";
}
}
}
// 使用示例
function setupResizablePanels() {
// 创建可调整大小的侧边栏
var sidebar = $("#sidebar");
var sidebarPanel = new ResizablePanel(sidebar, {
direction: "horizontal",
minSize: 200,
maxSize: 400,
initialSize: 300,
collapsible: true
});
// 监听调整事件
sidebar.on("resize", function(evt) {
stdout.println(`侧边栏大小调整: ${evt.detail.size}px`);
// 调整其他元素
var content = $("#main-content");
if (content) {
content.style.marginLeft = evt.detail.size + "px";
}
});
// 创建可调整大小的底部面板
var bottomPanel = $("#bottom-panel");
var bottomResizable = new ResizablePanel(bottomPanel, {
direction: "vertical",
minSize: 100,
maxSize: 300,
initialSize: 150
});
}
2. 拖拽布局
// 拖拽布局管理器
class DragLayout {
function this(container, options = {}) {
this.container = container;
this.options = Object.assign({
itemSelector: ".layout-item",
handleSelector: ".drag-handle",
placeholder: true,
animation: true,
grid: false,
snapToGrid: false,
gridSize: 10
}, options);
this.draggedItem = null;
this.placeholder = null;
this.dragOffset = { x: 0, y: 0 };
this.init();
}
function init() {
this.container.addClass("drag-layout");
this.setupEvents();
if (this.options.placeholder) {
this.createPlaceholder();
}
}
function createPlaceholder() {
this.placeholder = Element.create("div", {
class: "layout-placeholder"
});
}
function setupEvents() {
var self = this;
// 使用事件委托
this.container.on("mousedown", this.options.handleSelector || this.options.itemSelector, function(evt) {
if (evt.button === 0) { // 左键
self.startDrag(this, evt);
}
});
document.on("mousemove", function(evt) {
if (self.draggedItem) {
self.handleDrag(evt);
}
});
document.on("mouseup", function(evt) {
if (self.draggedItem) {
self.endDrag(evt);
}
});
}
function startDrag(handle, evt) {
// 找到要拖拽的项目
this.draggedItem = handle.closest(this.options.itemSelector);
if (!this.draggedItem) return;
// 计算拖拽偏移
var rect = this.draggedItem.getBoundingClientRect();
this.dragOffset.x = evt.clientX - rect.left;
this.dragOffset.y = evt.clientY - rect.top;
// 添加拖拽样式
this.draggedItem.addClass("dragging");
this.container.addClass("drag-active");
// 显示占位符
if (this.placeholder) {
this.placeholder.style.width = rect.width + "px";
this.placeholder.style.height = rect.height + "px";
this.draggedItem.parentNode.insertBefore(this.placeholder, this.draggedItem.nextSibling);
}
// 设置拖拽项目为绝对定位
this.draggedItem.style.position = "absolute";
this.draggedItem.style.zIndex = "1000";
this.updateDragPosition(evt);
evt.preventDefault();
}
function handleDrag(evt) {
this.updateDragPosition(evt);
// 查找插入位置
var insertPosition = this.findInsertPosition(evt);
if (insertPosition && this.placeholder) {
this.updatePlaceholderPosition(insertPosition);
}
}
function updateDragPosition(evt) {
var x = evt.clientX - this.dragOffset.x;
var y = evt.clientY - this.dragOffset.y;
// 网格对齐
if (this.options.snapToGrid) {
x = Math.round(x / this.options.gridSize) * this.options.gridSize;
y = Math.round(y / this.options.gridSize) * this.options.gridSize;
}
this.draggedItem.style.left = x + "px";
this.draggedItem.style.top = y + "px";
}
function findInsertPosition(evt) {
var items = this.container.$$(this.options.itemSelector);
var draggedRect = this.draggedItem.getBoundingClientRect();
for (var item of items) {
if (item === this.draggedItem) continue;
var itemRect = item.getBoundingClientRect();
// 检查是否在项目上方或左侧
if (this.isAbove(draggedRect, itemRect) || this.isLeftOf(draggedRect, itemRect)) {
return { before: item };
}
}
return { append: true };
}
function isAbove(draggedRect, itemRect) {
return draggedRect.top + draggedRect.height / 2 < itemRect.top + itemRect.height / 2;
}
function isLeftOf(draggedRect, itemRect) {
return draggedRect.left + draggedRect.width / 2 < itemRect.left + itemRect.width / 2;
}
function updatePlaceholderPosition(position) {
if (position.before) {
position.before.parentNode.insertBefore(this.placeholder, position.before);
} else if (position.append) {
this.container.append(this.placeholder);
}
}
function endDrag(evt) {
// 恢复项目样式
this.draggedItem.removeClass("dragging");
this.draggedItem.style.position = "";
this.draggedItem.style.left = "";
this.draggedItem.style.top = "";
this.draggedItem.style.zIndex = "";
this.container.removeClass("drag-active");
// 插入到新位置
if (this.placeholder && this.placeholder.parentNode) {
this.placeholder.parentNode.insertBefore(this.draggedItem, this.placeholder);
this.placeholder.remove();
}
// 触发布局改变事件
this.container.dispatchEvent(new CustomEvent("layout-change", {
detail: {
item: this.draggedItem,
newIndex: this.getItemIndex(this.draggedItem)
}
}));
this.draggedItem = null;
}
function getItemIndex(item) {
var items = this.container.$$(this.options.itemSelector);
return Array.from(items).indexOf(item);
}
// 获取当前布局
function getLayout() {
var items = this.container.$$(this.options.itemSelector);
return Array.from(items).map(function(item, index) {
return {
id: item.id || index,
element: item,
index: index
};
});
}
// 设置布局
function setLayout(layout) {
var fragment = document.createDocumentFragment();
for (var item of layout) {
if (item.element) {
fragment.append(item.element);
}
}
this.container.innerHTML = "";
this.container.append(fragment);
}
}
// 使用示例
function setupDragLayout() {
var layoutContainer = $("#layout-container");
var dragLayout = new DragLayout(layoutContainer, {
itemSelector: ".widget",
handleSelector: ".widget-header",
snapToGrid: true,
gridSize: 20
});
// 监听布局改变
layoutContainer.on("layout-change", function(evt) {
stdout.println(`项目移动: ${evt.detail.item.id} -> 位置 ${evt.detail.newIndex}`);
// 保存布局
saveLayoutToStorage(dragLayout.getLayout());
});
// 加载保存的布局
var savedLayout = loadLayoutFromStorage();
if (savedLayout) {
dragLayout.setLayout(savedLayout);
}
}
function saveLayoutToStorage(layout) {
var layoutData = layout.map(function(item) {
return {
id: item.id,
index: item.index
};
});
localStorage.setItem("layout", JSON.stringify(layoutData));
}
function loadLayoutFromStorage() {
var layoutData = localStorage.getItem("layout");
if (!layoutData) return null;
try {
return JSON.parse(layoutData);
} catch (error) {
stderr.println("加载布局失败: " + error.message);
return null;
}
}
5.4 本章总结
在本章中,我们学习了Sciter的窗口管理与布局系统:
窗口管理
- 窗口的创建、显示、隐藏和关闭
- 窗口间通信机制
- 窗口事件处理
- 多窗口应用架构
布局系统
- CSS布局增强(Flow布局)
- 响应式布局设计
- 动态布局组件
- 可调整大小的面板
高级功能
- 拖拽布局实现
- 布局状态管理
- 性能优化技巧
实践应用
- 复杂界面布局设计
- 用户体验优化
- 跨平台适配
5.5 练习题
基础练习
- 创建一个多窗口的文本编辑器应用
- 实现一个响应式的仪表板布局
- 制作一个可调整大小的分割面板组件
- 设计一个拖拽排序的任务列表
进阶练习
- 实现一个复杂的IDE界面布局
- 创建一个可视化的布局编辑器
- 设计一个多标签页的窗口管理系统
- 实现一个自适应的网格布局系统
挑战练习
- 创建一个完整的桌面环境界面
- 实现一个支持嵌套的可拖拽布局系统
- 设计一个跨窗口的拖拽功能
- 制作一个布局模板系统
下一章预告: 在第6章中,我们将学习Sciter的数据绑定与状态管理,包括数据绑定机制、状态管理模式,以及如何构建响应式的数据驱动应用。