简单来说,Godot 4一共有三种运行模式:工程管理、编辑、运行
有点意思的是,每次调试,只能在其中一种模式下运行
如果同时配置了编辑器与工程管理器,则会报错:
if (editor && project_manager) {
OS::get_singleton()->print(
"Error: Command line arguments implied opening both editor and project manager, which is not possible. Aborting.\n");
goto error;
}
这三种运行模式中,最简单的就是工程管理 - Project Manager
要以工程管理模式运行,在命令行中加入 -p 或--project-manager即可
在源码中,Main::setup中判断:
else if (I->get() == "-p" || I->get() == "--project-manager") { // starts project manager
project_manager = true;
}
然后创建ProjectManager
if (project_manager) {
Engine::get_singleton()->startup_benchmark_begin_measure("project_manager");
Engine::get_singleton()->set_editor_hint(true);
ProjectManager *pmanager = memnew(ProjectManager);
ProgressDialog *progress_dialog = memnew(ProgressDialog);
pmanager->add_child(progress_dialog);
sml->get_root()->add_child(pmanager);
DisplayServer::get_singleton()->set_context(DisplayServer::CONTEXT_PROJECTMAN);
Engine::get_singleton()->startup_benchmark_end_measure();
}
从运行结果来看,ProjectManager就是一个简单的Windows窗口应用程序
基于对窗口程序的理解,肯定是有地方创建了窗口,结果在display_server_windows源码中找到:
DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) {
DWORD dwExStyle;
DWORD dwStyle;
_get_window_style(window_id_counter == MAIN_WINDOW_ID, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MAXIMIZED, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) | (p_flags & WINDOW_FLAG_POPUP), dwStyle, dwExStyle);
RECT WindowRect;
WindowRect.left = p_rect.position.x;
WindowRect.right = p_rect.position.x + p_rect.size.x;
WindowRect.top = p_rect.position.y;
WindowRect.bottom = p_rect.position.y + p_rect.size.y;
int rq_screen = get_screen_from_rect(p_rect);
if (rq_screen < 0) {
rq_screen = get_primary_screen(); // Requested window rect is outside any screen bounds.
}
if (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) {
Rect2i screen_rect = Rect2i(screen_get_position(rq_screen), screen_get_size(rq_screen));
WindowRect.left = screen_rect.position.x;
WindowRect.right = screen_rect.position.x + screen_rect.size.x;
WindowRect.top = screen_rect.position.y;
WindowRect.bottom = screen_rect.position.y + screen_rect.size.y;
} else {
Rect2i srect = screen_get_usable_rect(rq_screen);
Point2i wpos = p_rect.position;
if (srect != Rect2i()) {
wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3);
wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3);
}
WindowRect.left = wpos.x;
WindowRect.right = wpos.x + p_rect.size.x;
WindowRect.top = wpos.y;
WindowRect.bottom = wpos.y + p_rect.size.y;
}
Point2i offset = _get_screens_origin();
WindowRect.left += offset.x;
WindowRect.right += offset.x;
WindowRect.top += offset.y;
WindowRect.bottom += offset.y;
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
WindowID id = window_id_counter;
{
WindowData &wd = windows[id];
wd.hWnd = CreateWindowExW(
dwExStyle,
L"Engine", L"",
dwStyle,
// (GetSystemMetrics(SM_CXSCREEN) - WindowRect.right) / 2,
// (GetSystemMetrics(SM_CYSCREEN) - WindowRect.bottom) / 2,
WindowRect.left,
WindowRect.top,
WindowRect.right - WindowRect.left,
WindowRect.bottom - WindowRect.top,
nullptr,
nullptr,
hInstance,
// tunnel the WindowData we need to handle creation message
// lifetime is ensured because we are still on the stack when this is
// processed in the window proc
reinterpret_cast<void *>(&wd));
if (!wd.hWnd) {
MessageBoxW(nullptr, L"Window Creation Error.", L"ERROR", MB_OK | MB_ICONEXCLAMATION);
windows.erase(id);
ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Failed to create Windows OS window.");
}
if (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) {
wd.fullscreen = true;
if (p_mode == WINDOW_MODE_FULLSCREEN) {
wd.multiwindow_fs = true;
}
}
if (p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) {
wd.pre_fs_valid = true;
}
if (is_dark_mode_supported() && dark_title_available) {
BOOL value = is_dark_mode();
::DwmSetWindowAttribute(wd.hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
}
#ifdef VULKAN_ENABLED
if (context_vulkan) {
if (context_vulkan->window_create(id, p_vsync_mode, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) {
memdelete(context_vulkan);
context_vulkan = nullptr;
windows.erase(id);
ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Failed to create Vulkan Window.");
}
wd.context_created = true;
}
#endif
#ifdef GLES3_ENABLED
if (gl_manager) {
if (gl_manager->window_create(id, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) {
memdelete(gl_manager);
gl_manager = nullptr;
windows.erase(id);
ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Failed to create an OpenGL window.");
}
window_set_vsync_mode(p_vsync_mode, id);
}
#endif
RegisterTouchWindow(wd.hWnd, 0);
DragAcceptFiles(wd.hWnd, true);
if ((tablet_get_current_driver() == "wintab") && wintab_available) {
wintab_WTInfo(WTI_DEFSYSCTX, 0, &wd.wtlc);
wd.wtlc.lcOptions |= CXO_MESSAGES;
wd.wtlc.lcPktData = PK_STATUS | PK_NORMAL_PRESSURE | PK_TANGENT_PRESSURE | PK_ORIENTATION;
wd.wtlc.lcMoveMask = PK_STATUS | PK_NORMAL_PRESSURE | PK_TANGENT_PRESSURE;
wd.wtlc.lcPktMode = 0;
wd.wtlc.lcOutOrgX = 0;
wd.wtlc.lcOutExtX = wd.wtlc.lcInExtX;
wd.wtlc.lcOutOrgY = 0;
wd.wtlc.lcOutExtY = -wd.wtlc.lcInExtY;
wd.wtctx = wintab_WTOpen(wd.hWnd, &wd.wtlc, false);
if (wd.wtctx) {
wintab_WTEnable(wd.wtctx, true);
AXIS pressure;
if (wintab_WTInfo(WTI_DEVICES + wd.wtlc.lcDevice, DVC_NPRESSURE, &pressure)) {
wd.min_pressure = int(pressure.axMin);
wd.max_pressure = int(pressure.axMax);
}
AXIS orientation[3];
if (wintab_WTInfo(WTI_DEVICES + wd.wtlc.lcDevice, DVC_ORIENTATION, &orientation)) {
wd.tilt_supported = orientation[0].axResolution && orientation[1].axResolution;
}
} else {
print_verbose("WinTab context creation failed.");
}
} else {
wd.wtctx = 0;
}
if (p_mode == WINDOW_MODE_MAXIMIZED) {
wd.maximized = true;
wd.minimized = false;
}
if (p_mode == WINDOW_MODE_MINIMIZED) {
wd.maximized = false;
wd.minimized = true;
}
wd.last_pressure = 0;
wd.last_pressure_update = 0;
wd.last_tilt = Vector2();
// IME.
wd.im_himc = ImmGetContext(wd.hWnd);
ImmAssociateContext(wd.hWnd, (HIMC)0);
wd.im_position = Vector2();
if (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN || p_mode == WINDOW_MODE_MAXIMIZED) {
RECT r;
GetClientRect(wd.hWnd, &r);
ClientToScreen(wd.hWnd, (POINT *)&r.left);
ClientToScreen(wd.hWnd, (POINT *)&r.right);
wd.last_pos = Point2i(r.left, r.top) - _get_screens_origin();
wd.width = r.right - r.left;
wd.height = r.bottom - r.top;
} else {
wd.last_pos = p_rect.position;
wd.width = p_rect.size.width;
wd.height = p_rect.size.height;
}
window_id_counter++;
}
return id;
}
窗口标题在project_manager源文件中构造函数ProjectManager::ProjectManager()设置:
DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager", "Application"));
继续往下看,居然是用godot的Control类,把界面给搭了出来:
Panel *panel = memnew(Panel);
add_child(panel);
panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("Background"), SNAME("EditorStyles")));
VBoxContainer *vb = memnew(VBoxContainer);
panel->add_child(vb);
vb->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE);
Control *center_box = memnew(Control);
center_box->set_v_size_flags(Control::SIZE_EXPAND_FILL);
vb->add_child(center_box);
tabs = memnew(TabContainer);
center_box->add_child(tabs);
tabs->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
local_projects_hb = memnew(HBoxContainer);
local_projects_hb->set_name(TTR("Local Projects"));
tabs->add_child(local_projects_hb);
{
// Projects + search bar
VBoxContainer *search_tree_vb = memnew(VBoxContainer);
local_projects_hb->add_child(search_tree_vb);
search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
HBoxContainer *hb = memnew(HBoxContainer);
hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
search_tree_vb->add_child(hb);
search_box = memnew(LineEdit);
search_box->set_placeholder(TTR("Filter Projects"));
search_box->set_tooltip_text(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character."));
search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
hb->add_child(search_box);
loading_label = memnew(Label(TTR("Loading, please wait...")));
loading_label->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts")));
loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
hb->add_child(loading_label);
// The loading label is shown later.
loading_label->hide();
Label *sort_label = memnew(Label);
sort_label->set_text(TTR("Sort:"));
hb->add_child(sort_label);
filter_option = memnew(OptionButton);
filter_option->set_clip_text(true);
filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
hb->add_child(filter_option);
Vector<String> sort_filter_titles;
sort_filter_titles.push_back(TTR("Last Edited"));
sort_filter_titles.push_back(TTR("Name"));
sort_filter_titles.push_back(TTR("Path"));
for (int i = 0; i < sort_filter_titles.size(); i++) {
filter_option->add_item(sort_filter_titles[i]);
}
PanelContainer *pc = memnew(PanelContainer);
pc->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
pc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
search_tree_vb->add_child(pc);
_project_list = memnew(ProjectList);
_project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
_project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
_project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
pc->add_child(_project_list);
}
{
// Project tab side bar
VBoxContainer *tree_vb = memnew(VBoxContainer);
tree_vb->set_custom_minimum_size(Size2(120, 120));
local_projects_hb->add_child(tree_vb);
const int btn_h_separation = int(6 * EDSCALE);
create_btn = memnew(Button);
create_btn->set_text(TTR("New Project"));
create_btn->add_theme_constant_override("h_separation", btn_h_separation);
create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
tree_vb->add_child(create_btn);
import_btn = memnew(Button);
import_btn->set_text(TTR("Import"));
import_btn->add_theme_constant_override("h_separation", btn_h_separation);
import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
tree_vb->add_child(import_btn);
scan_btn = memnew(Button);
scan_btn->set_text(TTR("Scan"));
scan_btn->add_theme_constant_override("h_separation", btn_h_separation);
scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
tree_vb->add_child(scan_btn);
tree_vb->add_child(memnew(HSeparator));
open_btn = memnew(Button);
open_btn->set_text(TTR("Edit"));
open_btn->add_theme_constant_override("h_separation", btn_h_separation);
open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
tree_vb->add_child(open_btn);
run_btn = memnew(Button);
run_btn->set_text(TTR("Run"));
run_btn->add_theme_constant_override("h_separation", btn_h_separation);
run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
tree_vb->add_child(run_btn);
rename_btn = memnew(Button);
rename_btn->set_text(TTR("Rename"));
rename_btn->add_theme_constant_override("h_separation", btn_h_separation);
// The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
tree_vb->add_child(rename_btn);
erase_btn = memnew(Button);
erase_btn->set_text(TTR("Remove"));
erase_btn->add_theme_constant_override("h_separation", btn_h_separation);
erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
tree_vb->add_child(erase_btn);
erase_missing_btn = memnew(Button);
erase_missing_btn->set_text(TTR("Remove Missing"));
erase_missing_btn->add_theme_constant_override("h_separation", btn_h_separation);
erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
tree_vb->add_child(erase_missing_btn);
tree_vb->add_spacer();
about_btn = memnew(Button);
about_btn->set_text(TTR("About"));
about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
tree_vb->add_child(about_btn);
}
{
// Version info and language options
settings_hb = memnew(HBoxContainer);
settings_hb->set_alignment(BoxContainer::ALIGNMENT_END);
settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT);
// A VBoxContainer that contains a dummy Control node to adjust the LinkButton's vertical position.
VBoxContainer *spacer_vb = memnew(VBoxContainer);
settings_hb->add_child(spacer_vb);
Control *v_spacer = memnew(Control);
spacer_vb->add_child(v_spacer);
version_btn = memnew(LinkButton);
String hash = String(VERSION_HASH);
if (hash.length() != 0) {
hash = " " + vformat("[%s]", hash.left(9));
}
version_btn->set_text("v" VERSION_FULL_BUILD + hash);
// Fade the version label to be less prominent, but still readable.
version_btn->set_self_modulate(Color(1, 1, 1, 0.6));
version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
version_btn->set_tooltip_text(TTR("Click to copy."));
version_btn->connect("pressed", callable_mp(this, &ProjectManager::_version_button_pressed));
spacer_vb->add_child(version_btn);
// Add a small horizontal spacer between the version and language buttons
// to distinguish them.
Control *h_spacer = memnew(Control);
settings_hb->add_child(h_spacer);
language_btn = memnew(OptionButton);
language_btn->set_icon(get_theme_icon(SNAME("Environment"), SNAME("EditorIcons")));
language_btn->set_focus_mode(Control::FOCUS_NONE);
language_btn->set_fit_to_longest_item(false);
language_btn->set_flat(true);
language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected));
#ifdef ANDROID_ENABLED
// The language selection dropdown doesn't work on Android (as the setting isn't saved), see GH-60353.
// Also, the dropdown it spawns is very tall and can't be scrolled without a hardware mouse.
// Hiding the language selection dropdown also leaves more space for the version label to display.
language_btn->hide();
#endif
Vector<String> editor_languages;
List<PropertyInfo> editor_settings_properties;
EditorSettings::get_singleton()->get_property_list(&editor_settings_properties);
for (const PropertyInfo &pi : editor_settings_properties) {
if (pi.name == "interface/editor/editor_language") {
editor_languages = pi.hint_string.split(",");
break;
}
}
String current_lang = EDITOR_GET("interface/editor/editor_language");
language_btn->set_text(current_lang);
for (int i = 0; i < editor_languages.size(); i++) {
String lang = editor_languages[i];
String lang_name = TranslationServer::get_singleton()->get_locale_name(lang);
language_btn->add_item(vformat("[%s] %s", lang, lang_name), i);
language_btn->set_item_metadata(i, lang);
if (current_lang == lang) {
language_btn->select(i);
}
}
settings_hb->add_child(language_btn);
center_box->add_child(settings_hb);
}
if (AssetLibraryEditorPlugin::is_available()) {
asset_library = memnew(EditorAssetLibrary(true));
asset_library->set_name(TTR("Asset Library Projects"));
tabs->add_child(asset_library);
asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project));
} else {
print_verbose("Asset Library not available (due to using Web editor, or SSL support disabled).");
}
{
// Dialogs
language_restart_ask = memnew(ConfirmationDialog);
language_restart_ask->set_ok_button_text(TTR("Restart Now"));
language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
language_restart_ask->set_cancel_button_text(TTR("Continue"));
add_child(language_restart_ask);
scan_dir = memnew(EditorFileDialog);
scan_dir->set_previews_enabled(false);
scan_dir->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
scan_dir->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden
scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path"));
add_child(scan_dir);
scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin));
erase_missing_ask = memnew(ConfirmationDialog);
erase_missing_ask->set_ok_button_text(TTR("Remove All"));
erase_missing_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm));
add_child(erase_missing_ask);
erase_ask = memnew(ConfirmationDialog);
erase_ask->set_ok_button_text(TTR("Remove"));
erase_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm));
add_child(erase_ask);
VBoxContainer *erase_ask_vb = memnew(VBoxContainer);
erase_ask->add_child(erase_ask_vb);
erase_ask_label = memnew(Label);
erase_ask_vb->add_child(erase_ask_label);
delete_project_contents = memnew(CheckBox);
delete_project_contents->set_text(TTR("Also delete project contents (no undo!)"));
erase_ask_vb->add_child(delete_project_contents);
multi_open_ask = memnew(ConfirmationDialog);
multi_open_ask->set_ok_button_text(TTR("Edit"));
multi_open_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects));
add_child(multi_open_ask);
multi_run_ask = memnew(ConfirmationDialog);
multi_run_ask->set_ok_button_text(TTR("Run"));
multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm));
add_child(multi_run_ask);
multi_scan_ask = memnew(ConfirmationDialog);
multi_scan_ask->set_ok_button_text(TTR("Scan"));
add_child(multi_scan_ask);
ask_update_settings = memnew(ConfirmationDialog);
ask_update_settings->set_autowrap(true);
ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
full_convert_button = ask_update_settings->add_button(TTR("Convert Full Project"), !GLOBAL_GET("gui/common/swap_cancel_ok"));
full_convert_button->connect("pressed", callable_mp(this, &ProjectManager::_full_convert_button_pressed));
add_child(ask_update_settings);
ask_full_convert_dialog = memnew(ConfirmationDialog);
ask_full_convert_dialog->set_autowrap(true);
ask_full_convert_dialog->set_text(TTR("This option will perform full project conversion, updating scenes, resources and scripts from Godot 3.x to work in Godot 4.0.\n\nNote that this is a best-effort conversion, i.e. it makes upgrading the project easier, but it will not open out-of-the-box and will still require manual adjustments.\n\nIMPORTANT: Make sure to backup your project before converting, as this operation makes it impossible to open it in older versions of Godot."));
ask_full_convert_dialog->connect("confirmed", callable_mp(this, &ProjectManager::_perform_full_project_conversion));
add_child(ask_full_convert_dialog);
npdialog = memnew(ProjectDialog);
npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
add_child(npdialog);
run_error_diag = memnew(AcceptDialog);
run_error_diag->set_title(TTR("Can't run project"));
add_child(run_error_diag);
dialog_error = memnew(AcceptDialog);
add_child(dialog_error);
if (asset_library) {
open_templates = memnew(ConfirmationDialog);
open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?"));
open_templates->set_ok_button_text(TTR("Open Asset Library"));
open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library));
add_child(open_templates);
}
about = memnew(EditorAbout);
add_child(about);
_build_icon_type_cache(get_theme());
}
代码与界面效果可一一对应。
除去界面各控件外,就是加载工程项_load_recent_projects(),直接处理
void ProjectList::load_projects() {
// This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
// If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
// Clear whole list
for (int i = 0; i < _projects.size(); ++i) {
Item &project = _projects.write[i];
CRASH_COND(project.control == nullptr);
memdelete(project.control); // Why not queue_free()?
}
_projects.clear();
_last_clicked = "";
_selected_project_paths.clear();
List<String> sections;
_config.load(_config_path);
_config.get_sections(§ions);
for (const String &path : sections) {
bool favorite = _config.get_value(path, "favorite", false);
_projects.push_back(load_project_data(path, favorite));
}
// Create controls
for (int i = 0; i < _projects.size(); ++i) {
create_project_item_control(i);
}
sort_projects();
set_v_scroll(0);
update_icons_async();
update_dock_menu();
}
我之前一直用RAD开发Windows程序,哪里还见过这种自己从头搭起来的,比如create_project_item_control处理各工程项
void ProjectList::create_project_item_control(int p_index) {
// Will be added last in the list, so make sure indexes match
ERR_FAIL_COND(p_index != _scroll_children->get_child_count());
Item &item = _projects.write[p_index];
ERR_FAIL_COND(item.control != nullptr); // Already created
Ref<Texture2D> favorite_icon = get_theme_icon(SNAME("Favorites"), SNAME("EditorIcons"));
Color font_color = get_theme_color(SNAME("font_color"), SNAME("Tree"));
ProjectListItemControl *hb = memnew(ProjectListItemControl);
hb->connect("draw", callable_mp(this, &ProjectList::_panel_draw).bind(hb));
hb->connect("gui_input", callable_mp(this, &ProjectList::_panel_input).bind(hb));
hb->add_theme_constant_override("separation", 10 * EDSCALE);
hb->set_tooltip_text(item.description);
VBoxContainer *favorite_box = memnew(VBoxContainer);
favorite_box->set_name("FavoriteBox");
TextureButton *favorite = memnew(TextureButton);
favorite->set_name("FavoriteButton");
favorite->set_texture_normal(favorite_icon);
// This makes the project's "hover" style display correctly when hovering the favorite icon.
favorite->set_mouse_filter(MOUSE_FILTER_PASS);
favorite->connect("pressed", callable_mp(this, &ProjectList::_favorite_pressed).bind(hb));
favorite_box->add_child(favorite);
favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
hb->add_child(favorite_box);
hb->favorite_button = favorite;
hb->set_is_favorite(item.favorite);
TextureRect *tf = memnew(TextureRect);
// The project icon may not be loaded by the time the control is displayed,
// so use a loading placeholder.
tf->set_texture(get_theme_icon(SNAME("ProjectIconLoading"), SNAME("EditorIcons")));
tf->set_v_size_flags(SIZE_SHRINK_CENTER);
if (item.missing) {
tf->set_modulate(Color(1, 1, 1, 0.5));
}
hb->add_child(tf);
hb->icon = tf;
VBoxContainer *vb = memnew(VBoxContainer);
if (item.grayed) {
vb->set_modulate(Color(1, 1, 1, 0.5));
}
vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
hb->add_child(vb);
Control *ec = memnew(Control);
ec->set_custom_minimum_size(Size2(0, 1));
ec->set_mouse_filter(MOUSE_FILTER_PASS);
vb->add_child(ec);
{ // Top half, title and unsupported features labels.
HBoxContainer *title_hb = memnew(HBoxContainer);
vb->add_child(title_hb);
Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
title->add_theme_color_override("font_color", font_color);
title->set_clip_text(true);
title_hb->add_child(title);
String unsupported_features_str = String(", ").join(item.unsupported_features);
int length = unsupported_features_str.length();
if (length > 0) {
Label *unsupported_label = memnew(Label(unsupported_features_str));
unsupported_label->set_custom_minimum_size(Size2(length * 15, 10) * EDSCALE);
unsupported_label->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
unsupported_label->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), SNAME("Editor")));
unsupported_label->set_clip_text(true);
unsupported_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
title_hb->add_child(unsupported_label);
Control *spacer = memnew(Control());
spacer->set_custom_minimum_size(Size2(10, 10));
title_hb->add_child(spacer);
}
}
{ // Bottom half, containing the path and view folder button.
HBoxContainer *path_hb = memnew(HBoxContainer);
path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
vb->add_child(path_hb);
Button *show = memnew(Button);
// Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
show->set_icon(get_theme_icon(!item.missing ? SNAME("Load") : SNAME("FileBroken"), SNAME("EditorIcons")));
show->set_flat(true);
if (!item.grayed) {
// Don't make the icon less prominent if the parent is already grayed out.
show->set_modulate(Color(1, 1, 1, 0.5));
}
path_hb->add_child(show);
if (!item.missing) {
#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
show->connect("pressed", callable_mp(this, &ProjectList::_show_project).bind(item.path));
show->set_tooltip_text(TTR("Show in File Manager"));
#else
// Opening the system file manager is not supported on the Android and web editors.
show->hide();
#endif
} else {
show->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
}
Label *fpath = memnew(Label(item.path));
fpath->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
path_hb->add_child(fpath);
fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
fpath->set_modulate(Color(1, 1, 1, 0.5));
fpath->add_theme_color_override("font_color", font_color);
fpath->set_clip_text(true);
}
_scroll_children->add_child(hb);
item.control = hb;
}
其实就是事无巨细,全抓
如果想用Godot来做界面的话,可以好好看下这些代码。
比如,对于已不存在的工程, 修改代码
Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
为
Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project") + " - " + item.path));
则显示效果为:
其余控件,就不再研究了,大致如此。
上面为初始化过程。至于其中的循环,因为在ProjectManager构造函数中,已设置
NavigationServer3D::get_singleton()->set_active(false);
PhysicsServer3D::get_singleton()->set_active(false);
PhysicsServer2D::get_singleton()->set_active(false);
所以,循环也就是窗口自己玩,直至这个条件满足
_project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
即触发信号SIGNAL_PROJECT_ASK_OPEN时会调用
_open_selected_projects_ask,其实最终调用_open_selected_projects
void ProjectManager::_open_selected_projects() {
// Show loading text to tell the user that the project manager is busy loading.
// This is especially important for the Web project manager.
loading_label->show();
const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
for (const String &path : selected_list) {
String conf = path.path_join("project.godot");
if (!FileAccess::exists(conf)) {
dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path));
dialog_error->popup_centered();
return;
}
print_line("Editing project: " + path);
List<String> args;
for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_TOOL)) {
args.push_back(a);
}
args.push_back("--path");
args.push_back(path);
args.push_back("--editor");
Error err = OS::get_singleton()->create_instance(args);
ERR_FAIL_COND(err);
}
_project_list->project_opening_initiated = true;
_dim_window();
get_tree()->quit();
}
其本质在于设置命令行参数--editor --path ...,调用新的实例后退出程序
args.push_back("--path");
args.push_back(path);
args.push_back("--editor");
Error err = OS::get_singleton()->create_instance(args);
这个命令行,就是以编辑器模式运行。
当然,还有ENTER键会直接调用_open_selected_projects_ask
void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
ERR_FAIL_COND(p_ev.is_null());
Ref<InputEventKey> k = p_ev;
if (k.is_valid()) {
if (!k->is_pressed()) {
return;
}
// Pressing Command + Q quits the Project Manager
// This is handled by the platform implementation on macOS,
// so only define the shortcut on other platforms
#ifndef MACOS_ENABLED
if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
_dim_window();
get_tree()->quit();
}
#endif
if (tabs->get_current_tab() != 0) {
return;
}
bool keycode_handled = true;
switch (k->get_keycode()) {
case Key::ENTER: {
_open_selected_projects_ask();
} break;
case Key::HOME: {
if (_project_list->get_project_count() > 0) {
_project_list->select_project(0);
_update_project_buttons();
}
} break;
case Key::END: {
if (_project_list->get_project_count() > 0) {
_project_list->select_project(_project_list->get_project_count() - 1);
_update_project_buttons();
}
} break;
case Key::UP: {
if (k->is_shift_pressed()) {
break;
}
int index = _project_list->get_single_selected_index();
if (index > 0) {
_project_list->select_project(index - 1);
_project_list->ensure_project_visible(index - 1);
_update_project_buttons();
}
break;
}
case Key::DOWN: {
if (k->is_shift_pressed()) {
break;
}
int index = _project_list->get_single_selected_index();
if (index + 1 < _project_list->get_project_count()) {
_project_list->select_project(index + 1);
_project_list->ensure_project_visible(index + 1);
_update_project_buttons();
}
} break;
case Key::F: {
if (k->is_command_or_control_pressed()) {
this->search_box->grab_focus();
} else {
keycode_handled = false;
}
} break;
default: {
keycode_handled = false;
} break;
}
if (keycode_handled) {
accept_event();
}
}
}
工程管理应该是最简单的,可以熟悉一下Godot的界面控件。