考虑一个充当用户提供的控件(如窗框)装饰器的控件。我希望我的控件具有窗口的所有常见逻辑(标题栏、可拖动窗口边框、隐藏窗口的按钮等),并且任何时候在主场景中实例化它时,我希望它“吃掉”其任何节点子节点并将它们放入我选择的容器中。
这是我制作的控件,LinesContainer容器是我希望其子级驻留的位置:
为了绝对清楚我的意思,当它被实例化到如下场景时,我希望它的子节点(在本例中为标签)表现得像节点的子节点LinesContainer一样:
如果您完全熟悉 .Net XAML,这就是标签ContentPresenter在控件中的作用,它“吃掉”了Content整个控件的属性(即控件实例的子级,如上所述)并将其显示在该标签内,允许我在它周围(或者在它后面,或者在它上面,等等)创建我需要的任何东西。
有什么内置的东西吗ContentPresenter?或者如果没有,我将如何自己制作一些东西?如果可能的话,这也可以在编辑器中正常工作,允许我根据需要添加和删除项目并使其布局正确。
以下是我按照您的要求做的最佳尝试,如所写。我已经在 Godot 4.1 和 4.2.dev 2 中进行了一些测试,并且它可以使用我将要讨论的一些警告。
但在开始之前,我想提一下,Container不推荐使用 base,因为它Container什么也不做。因此,您可以使用常规的Control. 是的,我提出的解决方案可以修改为与常规一起使用Control,我将在最后讨论。
附录:我想 Godot 标准方法是使用“可编辑子项”(您可以在场景停靠栏的场景实例的上下文菜单中找到它),我想提一下,以防万一您不知道这一点。另外,您想要的功能可以提出一个很好的提案:https://github.com/godotengine/godot-proposals/issues(至少考虑展开讨论:https://github.com/godotengine/godot-proposals/讨论)
设计注意事项和注意事项:
ContentPresenterXAML 中的 a ,我就调用新类ContentPresenter。Control,也不想重新设置它们的父级,因为这可能会导致问题(例如, aNodePath不再正确,或者控件上的脚本无法处理从场景树中删除)。Control。这一切都在新ContentPresenter班级里。Container,但是我正在处理可能发生变化的情况Container,因为您删除这部分代码比添加它更容易。也许你会在这个过程中学到一些东西。@tool脚本。@tool脚本对另一个节点有一个变量引用...我不会公开一个Container属性,而是一个NodePath属性(我在 Godot 4.2.dev5 中开发这个属性时尝试使用该Container属性时发生了崩溃 - 无论它是什么)希望在稳定版本中得到修复 - 但我又回到了旧的方式)。Container尚不清楚,因此我希望有一些实际上作为Container.dual的子级Container,并来回复制它们的位置属性。dual在场景树中看到 s。这是因为它们没有owner,因此也不会与场景一起存储。相反,它们将在场景加载时重新生成(无论是在编辑器中还是在运行时)。duals 映射到孩子们Control,我将其临时编译成Dictionary. 在我存储之前,Dictionary但遇到了它不同步的问题。我也确实考虑过使用s ,但是如果您想拥有已经存在的子级(例如) name,这可能会引起冲突。ContainerCursorContainer已经存在的孩子,代码会将duals放在它们后面children_to_skip(如果您想要它们放在前面,请用替换的计算0)。child_entered_tree,child_order_changed并child_order_changed确保正确的对偶以正确的顺序存在。minimum_size_changed以及触发item_rect_changed持仓属性复制的信号。这里item_rect_changed似乎resized多余了,我没有找到另一种方法来处理位置的变化(除了检查每一帧,这会降低效率)。_process)中,但是,我没有将其包含在这个答案中(代码_process大部分时间都会保持禁用状态以减少性能影响)。因此,大小标志的更改不会立即反映出来,您可以利用dual重新生成的 s 在编辑器中修复它,调用_invalidate_childdren也应该修复它。Control,则将其放在 s 的顶部Container是行不通的。这是代码:
@tool
class_name ContentPresenter
extends Control
# Reference to the container that children will behave as if they were in
var target_container:Container
# NodePath to the container that children will behave as if they were in
@export var target_container_path:NodePath:
set(mod_value):
# Update the NodePath
target_container_path = mod_value
# Update the reference et.al. only if this node is ready
# If this node is not ready, the reponsability falls to _ready
if is_node_ready():
_update_target_container()
var invalidated_children:bool:
set(mod_value):
if invalidated_children == mod_value:
return
invalidated_children = mod_value
var invalidated_container:bool:
set(mod_value):
if invalidated_container == mod_value:
return
invalidated_container = mod_value
func _invalidate_children() -> void:
invalidated_children = true
set_process(true)
func _invalidate_container() -> void:
invalidated_container = true
set_process(true)
# Runs when this node is ready
func _ready() -> void:
# Make sure child_entered_tree is connected
if not child_entered_tree.is_connected(_child_entered_tree):
child_entered_tree.connect(_child_entered_tree)
# Make sure child_exiting_tree is connected
if not child_exiting_tree.is_connected(_child_exiting_tree):
child_exiting_tree.connect(_child_exiting_tree)
# Make sure child_order_changed is connected
if not child_order_changed.is_connected(_child_order_changed):
child_order_changed.connect(_child_order_changed)
# Update the container reference if necessary
_update_target_container()
_invalidate_children()
# Runs when this node leaves the scene tree
func _exit_tree() -> void:
# Request to run _ready next time it enters the scene tree
# This is so it can update the reference to the container
request_ready()
# Called by the Godot edito to get warning
func _get_configuration_warnings() -> PackedStringArray:
# If we don't have a valid reference to the container put up a warning
if not is_instance_valid(target_container):
return ["Target Container Not Found"]
return []
func _process(_delta: float) -> void:
if is_instance_valid(target_container):
var control_by_dual := {}
var dual_by_control := {}
var duals_to_remove:Array[Control] = []
var children_to_skip := 0
for dual_candidate in target_container.get_children():
if dual_candidate.has_meta("__dual_of"):
var control := _validate_control(dual_candidate.get_meta("__dual_of", null))
if is_instance_valid(control):
control_by_dual[dual_candidate] = control
dual_by_control[control] = dual_candidate
else:
duals_to_remove.append(dual_candidate)
else:
children_to_skip += 1
if invalidated_container:
for dual in control_by_dual.keys():
var control:Control = control_by_dual[dual]
_copy_positioning(dual, control, false)
if invalidated_children:
# Make sure all the children Controls have a dual, and what should their order be
var order:Array[Control] = []
for control_candidate in get_children():
var control := _validate_control(control_candidate)
if not is_instance_valid(control):
continue
var dual:Control = dual_by_control.get(control, null)
if not is_instance_valid(dual):
dual = Control.new()
# When the child control changes its minimum size, update the dual
if not control.minimum_size_changed.is_connected(_invalidate_children):
control.minimum_size_changed.connect(_invalidate_children)
# When the child control moves or resizes, update the dual
if not control.item_rect_changed.is_connected(_invalidate_children):
control.item_rect_changed.connect(_invalidate_children)
# When the dual moves or resizes, update the child control
if not dual.item_rect_changed.is_connected(_invalidate_container):
dual.item_rect_changed.connect(_invalidate_container)
dual.set_meta("__dual_of", control)
control_by_dual[dual] = control
dual_by_control[control] = dual
target_container.add_child(dual)
#dual.owner = owner if owner != null else self
order.append(dual)
# Remove any duals whose child Control is no longer valid
for dual in duals_to_remove:
target_container.remove_child(dual)
dual.queue_free()
# Clear the list to remove so we don't remove them again
duals_to_remove = []
# Make sure the dual is in the correct order in the container children
for index in order.size():
target_container.move_child(order[index], children_to_skip + index)
# Update the duals position
for dual in control_by_dual.keys():
var control = control_by_dual[dual]
_copy_positioning(control, dual, true)
# Remove any duals whose child Control is no longer valid (if they weren't removed before)
for dual in duals_to_remove:
target_container.remove_child(dual)
dual.queue_free()
set_process(false)
# Called by _ready or target_container_path's setter
func _update_target_container():
# Figure out the new reference to the container
var new_target_container:Container = null
if not target_container_path.is_empty():
new_target_container = get_node_or_null(target_container_path)
# If it is the same reference do nothing
if new_target_container == target_container:
update_configuration_warnings()
return
# Since we are going to change container, remove duals from the old one
if is_instance_valid(target_container):
var children := target_container.get_children()
for child in children:
if child.has_meta("__dual_of"):
target_container.remove_child(child)
if target_container.item_rect_changed.is_connected(_invalidate_container):
target_container.item_rect_changed.disconnect(_invalidate_container)
# Update the container reference
target_container = new_target_container
if is_instance_valid(target_container):
if not target_container.item_rect_changed.is_connected(_invalidate_container):
target_container.item_rect_changed.connect(_invalidate_container)
_invalidate_container()
_invalidate_children()
# Tell Godot to update warning
update_configuration_warnings()
# Handler for child_entered_tree
func _child_entered_tree(node:Node) -> void:
var control := _validate_control(node)
if control == null:
return
_invalidate_children()
# Handler for child_exiting_tree
func _child_exiting_tree(node:Node) -> void:
var control := _validate_control(node)
if control == null:
return
_invalidate_children()
# Handler for child_order_changed
func _child_order_changed() -> void:
_invalidate_children()
# Called from _child_entered_tree and _child_exiting_tree
func _validate_control(node:Node) -> Control:
if node.owner == self:
# We got a node that is part of the scene
return null
var control = node as Control
if not is_instance_valid(control):
# We got a node that is not a Control
return null
if control.get_parent() != self:
return null
if (
is_instance_valid(target_container)
and (
control == target_container
or control.is_ancestor_of(target_container)
)
):
# We got a Control that contains the container
return null
# return the Control
return control
# Copies data between the children Controls and their duals
func _copy_positioning(from:Control, to:Control, is_push:bool) -> void:
# global transform of from
var from_global_transform := from.get_global_transform()
# global transform of the parent of to
var to_parent_global_transform := Transform2D.IDENTITY
var to_parent := to.get_parent_control()
if to_parent != null:
to_parent_global_transform = to_parent.get_global_transform()
# transform of from relative to the parent of to
var from_to_local_transform := to_parent_global_transform.affine_inverse() * from_global_transform
if is_push:
to.visible = from.visible
to.size_flags_horizontal = from.size_flags_horizontal
to.size_flags_vertical = from.size_flags_vertical
to.size_flags_stretch_ratio = from.size_flags_stretch_ratio
to.custom_minimum_size = from.get_combined_minimum_size()
to.size = from.size
to.global_position = from_global_transform.origin
to.rotation = from_to_local_transform.get_rotation()
to.scale = from_to_local_transform.get_scale()
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,我添加了一些评论,希望有助于理解正在发生的事情。
不过,我想进一步阐述一些事情:
is_node_ready在更新对 的引用之前进行了检查Container,这是因为我想在尝试访问它(以查询 )之前确保该节点位于场景树中NodePath。如果节点还没有准备好,那么_ready将调用该方法来更新引用。如果节点从场景树中删除(可能NodePath在不在场景树中时进行了修改)并再次添加,我需要再次更新引用,因为我用它来request_ready确保_ready再次运行(否则_ready只会运行第一次)。_validate_control检查 是否ContentPresenter是所有者,在 是根的Node场景中添加到编辑器中的 s就是这种情况。ContentPresenter所以这可以很容易地跳过那些Nodes。它还检查是否Control实际上是 的子级ContentPresenter,从而允许检测 a 是否dual指向已删除的实例Control,否则该实例仍然是有效的实例。_copy_positioning确实是其核心(并且需要更多时间来弄清楚)。如果您甚至不需要所有额外的设置,它可能对您有用。我会讲到这一点。_process方法将通过set_process(false)调用并禁用自身_invalidate_children或_invalidate_container再次启用它。_get_configuration_warningsGodot 将调用该方法来获取警告。它们显示为Node场景树中 旁边的黄色三角形。现在,如果您不需要目标来获取 a Container,则只需在几个地方更改它:
var target_container:Containervar new_target_container:Container = null另外,_get_configuration_warnings变量和方法的 、 和名称中也提到了它,但这些不是功能性的。
dual.item_rect_changed如果目标不是 a ,您还可以删除连接的行Container,假设只有Containers 会移动或调整其子级的大小。
我还想指出,将其作为转换来处理是有问题的。这将是一个无限的反馈循环:
Control。Control。Control,重复。因此,我需要跟踪原始值,并且以一种仍然允许您编辑它们的方式。因此,我相信duals 是一个很好的解决方案。
我最初发布这个答案时没有使用_process,但我遇到了信号的执行顺序问题。哪些重要:假设孩子Container和孩子都Control移动了,哪个先从另一个更新很重要。
现在使用_process我优先考虑Container移动孩子Control,这可以最大限度地减少他们行为不正确的情况。
也许你实际上并不需要照顾多个孩子。相反,您可能只想将一个Control(例如Container)的位置复制到另一个Control(例如您放置在 的“内部”的位置Container)。在这种情况下,该方法_copy_positioning可能对您仍然有用......但您可以摆脱所有设置dual。
因此,我请您考虑指定您将Container使用 a放入其中的内容NodePath。
不,如果您希望成为 的父级,则将Container位置复制到 是ContentPresenter行不通的。但是,如果您可以不将 当作 的子项,那么可能还有另一种方法隐藏在那里。例如,它可能类似于( / ),但对于s。但无论如何,这不是所写的问题。ContentPresenterContainerContainerContentPresenterRemoteTransform2D3DControl