如果需要在 SketchUp 插件中使用复杂的UI设计,不可避免地要使用UI::HtmlDialog。这不仅仅需要编写 Ruby 代码,还需要完成 HTML、CSS 和 JavaScript 的部分。本篇以编辑图元属性这个简单功能为案例,提供一个 HtmlDialog 的自学案例,仅供参考。
一、图元属性的编辑
在实现 UI 功能之前,要先把修改 SketchUp 模型的功能代码完成。本篇需要实现的功能很简单,只需要读取和写入选中图元的属性即可。一个图元包含一个属性组列表(AttributeDictionaries),其中可能包含若干个属性组(AttributeDictionary),每一个属性组中有若干个键值对,可以通过.to_h方法将其导出为 ruby 的键值对格式 Hash。代码如下:
# 在模型中选择一个动态组件实例
ent = Sketchup.active_model.selection[0]
ent.attribute_dictionaries.entries.map(&:name)
ent.attribute_dictionaries["dynamic_attributes"].to_h
显示结果如下:
["SU_InstanceSet", "dynamic_attributes", ""]
{"_has_movetool_behaviors"=>0.0, "_lengthunits"=>"CENTIMETERS", "_name"=>"组件#1", "lenx"=>31.889763779527556}
编辑图元属性需要使用.set_attribute方法,代码如下:
# 在模型中选择一个动态组件实例
ent = Sketchup.active_model.selection[0]
ent.set_attribute("dynamic_attributes","len",16.0)
由此可见,一个值的读取和保存相当于需要两个键名,一个是属性组的名称,一个是属性组内部的键名。这种设计可以让不同插件在保存自己生成的信息时不容易出现命名冲突,每一个插件都应该创建自己的属性组,在这个属性组内保存键值对。
现在明确一下这个属性编辑器的 UI 需要实现什么:
①确定显示和编辑的属性组名称,以及内部的键名;
②在点击图元时读取这些属性,在取消选择时清除显示;
③包含两个按键,一个用于提交属性值的修改,一个重置属性值的显示。
在明确以上需求后,创建一个名为 AttributeEditor.rb 的脚本文件,将其保存在 K:\ruby\ApiglioTest\ 目录下用于测试。文件内创建AttributeEditor模块,将第一个需求保存在模块变量@dict_and_attr中:
module AttributeEditor
@dict_and_attr = {"属性组"=>["键名1","键名2","键名3"]}
end
将 ruby 代码中的属性值数据传给 HtmlDialog 中的 JavaScript 代码时,涉及复杂的结构的情况下,需要使用 Hash 类对应 JavaScript 的 Object 类数据。因此在模块中创建.entity_to_jsobj和.apply_jsobj_to_entity两个方法,前者将一个图元的属性值打包成一个 Hash/Object,后者根据 Hash/Object 修改图元的属性值,代码如下:
module AttributeEditor
@dict_and_attr = {"属性组"=>["键名1","键名2","键名3"]}
def self.entity_to_jsobj(ent)
res={"type"=>ent.typename, "attr"=>{}}
unless ent.attribute_dictionaries.nil?
ent.attribute_dictionaries.entries.each{|dict|
res["attr"][dict.name]={}
dict.each{|k,v| res["attr"][dict.name][k]=v }
}
end
res
end
def self.apply_jsobj_to_entity(ent, js_obj)
js_obj["attr"].each{|dict_name, kvpairs|
kvpairs.each{|k,v|
old_value = ent.get_attribute(dict_name,k)
ent.set_attribute(dict_name,k,v) unless v == old_value
}
}
end
end
以下为测试代码与测试结果:
load "K:/ruby/ApiglioTest/AttributeEditor.rb"
ent = Sketchup.active_model.selection[0]
AttributeEditor.entity_to_jsobj(ent)
读取图元属性为 Ruby Hash / JavaScript Object
可以发现.entity_to_jsobj方法将所有属性组的键值对保存在"attr"键之下,同时在"type"键之下保存图元类型信息。额外的信息不会影响 HtmlDialog 的功能,JavaScript 只要按需读取相应的信息即可。
二、使用HTML设计界面
接下来需要创建 html 文件和其他与 UI 相关的内容。首先在 K:\ruby\ApiglioTest\ 目录下新建 UI\ 文件夹,在其中新建 AttributeEditor.html 文件并键入以下内容:
<html>
<head>
<meta charset=utf-8" />
head>
<body>
<header>Attribute Editorheader>
<div id="content">div>
<footer>
<button onclick="apply_on_click()">应用button>
<button onclick="reset_on_click()">重置button>
footer>
<script src="AttributeEditor.js">script>
body>
html>
以上 html 代码定义了窗体的页眉(header)和页脚(footer),中间部分的分割标签(div)指派其 id 为"content",这部分用于显示图元的属性值。在页脚创建两个按钮分别用于重置显示和提交修改,其中的onclick表示按下按键后执行的 JavaScript 脚本,这部分会在 UI\AttributeEditor.js 中实现。
在AttributeEditor模块中新增.showUI方法,并在模块定义的最后(17 - 30行)加上创建工具按钮的相关代码。
module AttributeEditor
def self.showUI()
unless defined?(@window) then
@window = UI::HtmlDialog.new(
{
:dialog_title => "Attribute Editor",
:scrollable => true,
:resizable => true,
:width => 300,
:height => 600,
:style => UI::HtmlDialog::STYLE_UTILITY
})
@window.set_file(__dir__+"/UI/AttributeEditor.html")
end
@window.show
end
unless defined?(@command) then
@command = UI::Command.new("Open Editor Window") {
self.showUI
}
@command.menu_text = "打开属性编辑器窗口"
@command.tooltip = "打开属性编辑器窗口"
@command.status_bar_text = "打开 Apiglio Attribute Editor 的属性编辑器窗口"
@command.small_icon = __dir__+"/UI/AttributeEditor_S.png" if File.exists?(__dir__+"/UI/AttributeEditor_S.png")
@command.large_icon = __dir__+"/UI/AttributeEditor_L.png" if File.exists?(__dir__+"/UI/AttributeEditor_L.png")
@command.set_validation_proc { MF_ENABLED }
@toolbar = UI::Toolbar.new("Apiglio Attribute Editor")
@toolbar = @toolbar.add_item(@command)
@toolbar.show
end
end
在.showUI方法中,先创建了一个 UI ::HtmlDialog 类,规定这个窗口的标题、尺寸等设置,最后读取 UI\AttributeEditor.html 文件并显示窗体。执行代码后可以看到 SketchUp 界面中新创建了一个按钮,点击这个按钮就能打开调用.showUI方法并打开之前设计的 html 窗口。
最简易的显示结果
如上图,此时只有页眉和页脚而没有中间部分,这是因为实现中间部分的 JavaScript 代码还没有编写。
三、编写JavaScript代码
前文的 html 文件中出现两个代表按钮动作的 JavaScript 函数:apply_on_click和reset_on_click,这两个函数需要在 UI\AttributeEditor.js 文件中实现:
function reset_on_click(){
sketchup.read_entity();
}
function apply_on_click(){
sketchup.write_entity(export_data());
}
其中export_data()函数的功能是将 html 界面的属性值转换成 Hash/Object 的格式,从而回传给 ruby 进行属性修改,具体实现见后文。首先关注skecthup .read_entity和sketchup .write_entity这两个函数,这是通过 JavaScript 调用 ruby 代码,需要在.showUI方法之内、@windowck.show之前增加以下代码以建立关联:
module AttributeEditor
def self.showUI
'...'
@window.add_action_callback("read_entity"){
|action_context|
self.read_entity()
}
@window.add_action_callback("write_entity"){
|action_context, obj|
self.write_entity(obj)
}
end
end
以上代码中.add_action_callback方法沟通 Ruby 和 JavaScript 代码。在本案例中,执行sketchup.write_entity的 js 代码就相当于执行self.read_entity的 ruby 代码,js 代码中的export_data返回值传给 ruby 代码时即参数obj。至于ruby代码中的两个同名方法,是AttributeEditor模块的方法,定义如下:
module AttributeEditor
def self.read_entity()
sels = Sketchup.active_model.selection
if sels.length==1 then
obj = entity_to_jsobj(sels[0])
if defined?(@window) then
@window.execute_script("reset_items(#{@dict_and_attr.to_json})")
@window.execute_script("update_data(#{obj.to_json});")
end
else
@window.execute_script("clear_items()")
end
end
def self.write_entity(obj)
sels = Sketchup.active_model.selection
return false unless sels.length==1
self.apply_jsobj_to_entity(sels[0],obj)
end
end
按下“应用”按钮时,会执行 js 的apply_on_click(),继而执行 js 的sketchup .write_entity(),继而执行ruby的self.write_entity(ent),而后根据以上代码(15 - 17行),先判断用户界面是否只选择一个图元,是则执行本文第一部分创建的.apply_jsobj_to_entity方法。
按下“重置”按钮时,情况会复杂一些。如果用户界面仅选择一个图元,需要修改 html 窗口中的属性值显示;如果不是,则需要清空 html 窗口的中间部分。两种情况都需要调用 UI\AttributeEditor.js 文件中定义的其他函数。具体来说就是reset_items、updata_data和clear_items这三个函数,分别用于初始化界面、显示图元属性和清空显示;再加上前文提到的的export_data函数,用于创建、修改和导出中间部分的功能总共有四个函数。
在实现这四个函数之前,需要先写两个辅助函数append_dictionary和append_attribute,这两个函数用于在中间部分创建属性组和键值对的相应结构。
function append_dictionary(dict_name){
let content = document.getElementById("content");
let dictdiv = document.createElement("div");
dictdiv.className = "dictionary";
dictdiv.id = dict_name;
dictdiv.innerHTML = `${dict_name}`
content.appendChild(dictdiv);
}
function append_attribute(dict_name, attr_name){
let dictdiv = document.getElementById(dict_name);
let attrdiv = document.createElement("div");
attrdiv.className = "attribute";
attrdiv.id = dict_name+"."+attr_name;
attrdiv.setAttribute("dict",dict_name);
attrdiv.setAttribute("attr",attr_name);
attrdiv.innerHTML = `
${attr_name}`dictdiv.appendChild(attrdiv);
}
以上 js 代码的大致逻辑是:找到 id 为"content"的分割标签(div),在其内部创建类型为"dictionary"的分割标签以表示属性组,设置其 id 为属性组的名称,并在开头以二级标题(h2)显示属性组名称。在表示属性组的分割标签内创建类型为"attribute"的分割标签,设置相应的 id 属性。并在其内部创建分割标签和文本区域(textarea)以显示键名和键值,二者的类型名分别为"key"和"value",其中显示键值的文本区域可由用户修改内容。类名、 id 和一些属性值的设置不仅用于查找,在有 CSS 的情况下还可以调整 html 的页面样式,做更个性化的样式设置。
以上两个函数规定了 html 页面中间部分的层级结构,在这个结构的基础上,就能进一步实现前文所说的四个函数:
function reset_items(dict_and_attr){
document.getElementById('content').innerHTML='';
for(dict_name in dict_and_attr){
append_dictionary(dict_name);
for(attr_name of dict_and_attr[dict_name]){
append_attribute(dict_name, attr_name);
}
}
}
function clear_items(){
let list_attrdiv = document.querySelectorAll('div.attribute');
for(attrdiv of list_attrdiv){
text_area = attrdiv.querySelector('textarea')
text_area.value = "";
text_area.setAttribute("status","lock");
}
}
function update_data(jsobj){
let list_attrdiv = document.querySelectorAll('div.attribute');
for(attrdiv of list_attrdiv){
dict_name = attrdiv.getAttribute("dict");
attr_name = attrdiv.getAttribute("attr");
text_area = attrdiv.querySelector('textarea');
if(dict_name in jsobj["attr"] && attr_name in jsobj["attr"][dict_name]){
text_area.value = jsobj["attr"][dict_name][attr_name];
text_area.setAttribute("status","");
}
}
}
function export_data(){
result = {};
let list_attrdiv = document.querySelectorAll('div.attribute');
for(attrdiv of list_attrdiv){
dict_name = attrdiv.getAttribute("dict");
attr_name = attrdiv.getAttribute("attr");
text_area = attrdiv.querySelector('textarea');
if(!(dict_name in result)){result[dict_name]={};}
result[dict_name][attr_name]=text_area.value;
}
return {"attr":result};
}
在reset_items函数中,js 根据.read_entity方法传递的@dict_and_attr参数,分层创建属性组和键值对的界面,由于@dict_and_attr中之规定了 1 个属性组及其名下的 3 个键值对,因此第 3 行的循环只会执行 1 次,第 5 行的循环则执行 3 次。而.read_entity方法中使用.to_json方法是为了将参数转化成 js 可读的形式。
在clear_items函数中,查找整个 html 中类型名为"attribute"的分割标签(div),并将其保存在list_attrdiv变量中,迭代将各自名下的文本区域(textarea)清空,并将status参数设置为"lock"。这么做是为了配合 CSS 实现“被禁用”的效果,如果不配置 CSS 这不需要。
updata_data 函数的逻辑与clear_items类似,通过查找符合条件的文本区域(textarea),在给定的jsobj参数中查找对应的值,分别对文本区域的值进行修改。
export_data函数的逻辑与updata_data相反,通过读取所有符合条件的文本区域(textarea)值,从而创建一个可以返回给 ruby 的 Hash/Object。
根据以上全部代码即可创建一个基本的属性编辑界面,使用时需要先选择一个图元,再点击“重置”按钮,如此便可以显示该图元的信息。这样便完成了前文的第三个需求。
一个初步的功能实现(右侧为html的调试窗口,右键菜单“Inspect Element”可调出)
也可以在以上内容的基础上增加 CSS,只需要在 html 文件中增加一行,并在 UI\AttributeEditor.css 中对每一类标签的样式进行规定,就能产生自定义的窗口效果。CSS 的具体设计就不在本篇的讨论范围内了,下图中的测试 CSS 文件可以在文末链接中找到。
<head>
<meta charset=utf-8" />
<link rel="stylesheet" type="text/css" href="AttributeEditor.css">
head>
一个包含css的功能实现
四、选区检测
以上功能每次选择图元后仍然需要手动点击“重置”按钮,这绝对不是一个易用的功能,也没有实现前文的第二个需求。因此需要在以上内容的基础上增加一个选区检测功能:当选区发生变化时,自动更新窗口的显示。
实现的方法很简单,在AttributeEditor模块中定义一个SelectionObserver类,在模块定义的最后创建这个 observer 并保存为模块变量@sel_observer。
module AttributeEditor
class SelectionObserver < Sketchup::SelectionObserver
def onSelectionBulkChange(selection)
AttributeEditor.read_entity()
end
def onSelectionCleared(selection)
AttributeEditor.read_entity()
end
end
unless defined?(@sel_observer) then
@sel_observer = self::SelectionObserver.new
end
end
之后在.showUI方法内@windowck.show之前的位置增加启用@sel_observer的相关代码即可:
module AttributeEditor
def self.showUI
'...'
@window.set_on_closed{
Sketchup.active_model.selection.remove_observer(@sel_observer)
}
Sketchup.active_model.selection.add_observer(@sel_observer)
@window.show
end
end
增加 SelectionObserver 之后,就能实现文章开头视频中的效果。最后,本期完整的代码过多并且有目录结构的要求,如有测试需要,代码获取详见文后说明。
(完)
从SU-2022-01期开始,本人的 SketchUp Ruby “小功能与灵感” 的文章中涉及的功能会在 Gitee/GitHub 中相应更新。这样一来,如果日后发现存在bug需要修正或者是想追加相关的功能,都有极大的便利。
这个代码仓库同样也包含大部分之前文章的代码,可以访问以下网址查看,也欢迎大家共同提交修改:
本期案例中的 ruby 代码在 apui.rb文件中,html、css、javascript以及图标等资源在UI目录下以 AttributeEditor 命名。直接加载 apui.rb 文件会加载 APUI 模块中的所有工具,如需测试文中代码,建议将其中的 AttributeEditor 模块单独保存。涉及文件包括:
apui.rb
UI/AttributeEditor.html
UI/AttributeEditor.js
UI/AttributeEditor.css
UI/AttributeEditor_L.png
UI/AttributeEditor_S.png
SketchUp ruby的中文资料相对较少,并且相对小众,欢迎加入 SketchUp Ruby 自学俱乐部 Q群(群号:368842817)。
本文编号:SU-2024-01
CSS 相对定位属性解析:relative 和 z-index
CSS 相对定位属性解析:relative 和 z-index,需要具体代码示例
引言:
在网页设计中,我们有时需要调整元素的位置和显示层级。CSS 相对定位属性可以帮助我们实现这些效果。本文将详细解析 CSS 相对定位属性中的 relative 属性和 z-index 属性,并提供具体的代码示例。
一、relative 属性的作用和用法
CSS 中的 relative 属性可以帮助我们相对于元素原本的位置进行微调。相对定位并不会脱离标准的文档流,元素的实际占位保持不变。下面是 relative 属性的常用语法:
.element { position: relative; top: 20px; left: 10px; }
登录后复制
在上述代码中,.element 是需要设置相对定位的元素,top 和 left 属性分别表示元素相对于原本位置的向下和向右的偏移量。
使用 relative 属性的一个常见示例是为图片添加一个文字描述,并将描述放置在图片正下方。代码示例如下:
登录后复制
.image-container { position: relative; width: 200px; height: 200px; } .caption { position: relative; top: 100%; text-align: center; }
登录后复制
在上述代码中,我们通过 .image-container 设置了相对定位,并通过 .caption 元素的 top: 100% 将文字描述置于图片正下方。
二、z-index 属性的作用和用法
CSS 中的 z-index 属性用于设置元素的层级顺序。具有较大 z-index 值的元素将覆盖具有较小 z-index 值的元素。下面是 z-index 属性的常用语法:
.element { position: relative; z-index: 2; }
登录后复制
在上述代码中,.element 是需要设置层级顺序的元素,z-index 属性用来指定一个数值,表示元素的层级顺序。
使用 z-index 属性可以实现元素的层叠效果。例如,我们可以创建一个简单的图层叠放效果,代码示例如下:
登录后复制
.box { position: relative; width: 100px; height: 100px; margin-bottom: 20px; } .red { background-color: red; z-index: 1; } .green { background-color: green; z-index: 2; } .blue { background-color: blue; z-index: 3; }
登录后复制
在上述代码中,我们创建了三个带有不同背景颜色的方块,并为每个方块设置了不同的 z-index 值。由于 .blue 具有最大的 z-index 值,它将显示在最上方,.green 在中间,.red 在最下方。
总结:
CSS 相对定位属性的 relative 值和层级顺序属性的 z-index 值在网页设计中有着重要的作用。通过 relative 属性,我们可以微调元素的位置。通过 z-index 属性,我们可以控制元素的层叠顺序。学会灵活运用这两个属性,可以使网页更加丰富多样。
以上就是关于 CSS 相对定位属性 relative 和 z-index 的解析,希望对读者有所帮助。在实际开发中,可以根据具体需求灵活运用这些属性,实现更精彩的网页设计效果。