Chrome学习小记4:基于Chrome Views框架创建最小示例窗口B(动手写代码)
前言
上一篇博客中,我们理解了一下example_main做的事情,现在我们就要进一步自己动手写代码了!那我们如何创建窗口呢?我们点击example里面具体做的事情,我们发现,是一个叫做views::Widget的类完成了我们的创口的本体工作。想要让我们的窗口出现自己的绘制样式,就需要一个代理绘制来完成我们的工作。这里笔者就略去了思考的流程(比如说我是怎么爬example找到关键点的,没啥技术含量更没意思,纯粹依靠Clangd的跳转解析),我们单刀直入话题。
我们的工作核心就是views::Views,views::Delegate和views::Widget三个基本的类。我们会依次的来看看这几个类的工作情况,来决定我们的参数如何抉择
views::Widget 绘制层的Window
我们打开ui\views\widget\widget.h文件仔细观察一下,开幕我们就能看到我们关心的views::Widget。谷歌的开发者指出了这些要点:
Widget
是一个平台独立的类型,它与特定于平台或上下文的 NativeWidget
实现进行通信。拥有一个 RootView
,从而拥有一个视图层级结构。可以包含子 Widget
。必须注意的是——所有 Widget
都应使用 ownership = CLIENT_OWNS_WIDGET
。创建 Widget
的客户端代码应该持有 std::unique_ptr<Widget>
。关闭 Widget
的正确方法是重置(reset
)该 unique_ptr
。Close()
和 CloseWithReason()
方法存在问题,因为它们会异步关闭 widget
。这意味着客户端代码必须处理 widget
已关闭但尚未销毁的边缘情况。
我们的Widget
有所有权这个事情,根据 InitParams
的 ownership
字段值,Widget
要么拥有其 NativeWidget
,要么被 NativeWidget
拥有。下面的参数我从Google Doc里摘录出来的:
ownership = NATIVE_WIDGET_OWNS_WIDGET
(默认):Widget
实例由其NativeWidget
拥有。当NativeWidget
被销毁时(响应原生销毁消息),它会在其析构函数中删除Widget
。ownership = WIDGET_OWNS_NATIVE_WIDGET
(非默认):Widget
实例拥有其NativeWidget
。此状态意味着有其他方希望控制此对象的生命周期。当它们销毁Widget
时,Widget
负责在其析构函数中销毁NativeWidget
。这通常用于将Widget
放置在std::unique_ptr<>
中或在测试中放置在栈上。
说的很干,我们如何初始化一个widget呢?
下面的代码直接说明了我们如何创建一个widget
auto* widget = new views::Widget();
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
params.bounds = gfx::Rect(100, 100, 1000, 700); // 设置出现在(100, 100),宽1000高700
// ------- delegate is creating here ----------
// delegate = ...
// --------------------------------------------
params.delegate = delegate; // 我们的下一个环节在这里。。。
widget->Init(std::move(params));
widget->Show();
Widget是需要我们默认指定InitParams结构体的,那我们就需要看看,如何构造一个合法的InitParams呢?答案是看example的做法,我们看到InitParams需要指定两个枚举,我在这里列出来,直接看:
struct VIEWS_EXPORT InitParams {
enum Type {
TYPE_WINDOW, // A decorated Window, like a frame window.
// Widgets of TYPE_WINDOW will have a NonClientView.
TYPE_WINDOW_FRAMELESS, // An undecorated Window.
TYPE_CONTROL, // A control, like a button.
TYPE_POPUP, // An undecorated Window, with transient properties.
TYPE_MENU, // An undecorated Window, with transient properties
// specialized to menus.
TYPE_TOOLTIP,
TYPE_BUBBLE,
TYPE_DRAG, // An undecorated Window, used during a drag-and-drop to
// show the drag image.
};
enum Ownership {
// The client (caller) manages the lifetime of the Widget, typically via
// std::unique_ptr<Widget>. This is the preferred ownership mode.
//
// If you encounter problems with this ownership mode, please file a bug.
//
// - The Widget remains valid even after the platform window
// (HWND, NSWindow, etc.) is closed.
// - Widget API calls are safe after the platform window closes, but
// most will become no-ops (e.g., Show() will do nothing).
// - The NativeWidget is destroyed when the platform window closes.
// - When the client destroys the Widget, a close request is sent to the
// platform window (if it's still open).
CLIENT_OWNS_WIDGET,
// The NativeWidget manages the lifetime of the Widget. The Widget is
// destroyed when the corresponding NativeWidget is destroyed.
//
// DEPRECATED: Prone to memory issues. A Widget* can be invalidated
// at any time, leading to dangling pointers. This does not fit typical
// C++ memory management idioms.
NATIVE_WIDGET_OWNS_WIDGET,
// The Widget owns the NativeWidget. The NativeWidget is destroyed when
// the corresponding Widget is destroyed.
//
// DEPRECATED: Causes problems with platform window shutdown. The OS
// usually does not expect the NativeWidget to be destroyed immediately
// when the platform window is closed. For example, if the platform window
// has a close animation, it must remain valid until the animation
// finishes to avoid prematurely destroying the compositor and its layer.
// This would also cause other platform-specific issues
// (e.g., crbug.com/40619853).
WIDGET_OWNS_NATIVE_WIDGET,
};
Widget::Type
枚举了 Widget
可能的不同类型,这些类型决定了 Widget
在视觉表现和行为上的特点。这些类型主要区分了窗口和控件,以及窗口是否有边框和装饰。
类型 | 描述 | 特点 | 例子 |
---|---|---|---|
TYPE_WINDOW | 带装饰的窗口 | 拥有标题栏、边框等,包含 NonClientView 。 | 应用程序的主窗口 |
TYPE_WINDOW_FRAMELESS | 无装饰的窗口 | 没有标准标题栏和边框,需要自定义绘制。 | 媒体播放器窗口 |
TYPE_CONTROL | UI 控件 | 用作其他 Widget 的子组件,如按钮、文本框。 | 按钮、滑块 |
TYPE_POPUP | 无装饰的临时窗口 | 依附于其他窗口,失去焦点后自动关闭。 | 下拉菜单、上下文菜单 |
TYPE_MENU | 专用的菜单窗口 | 类似 TYPE_POPUP ,但专门用于菜单。 | 应用程序的菜单栏 |
TYPE_TOOLTIP | 工具提示窗口 | 用于显示简短信息的无装饰小窗口。 | 鼠标悬停在按钮上时显示的提示 |
TYPE_BUBBLE | 气泡窗口 | 带箭头的无装饰临时窗口。 | 聊天应用的对话气泡 |
TYPE_DRAG | 拖放窗口 | 用于在拖放操作中显示拖动图像。 | 拖动文件时跟随鼠标的半透明图标 |
Widget::Ownership
枚举定义了 Widget
与其对应的原生平台窗口 (NativeWidget
) 之间生命周期管理的模式。这是理解 Widget
生命周期和避免内存问题的关键。
类型 | 描述 | 状态 | 优点/问题 |
---|---|---|---|
CLIENT_OWNS_WIDGET | 客户端拥有 Widget | 首选 | 优点:使用 std::unique_ptr 管理,避免悬空指针,生命周期可控且健壮。Widget 对象在平台窗口关闭后依然有效,直到客户端主动销毁。 |
NATIVE_WIDGET_OWNS_WIDGET | NativeWidget 拥有 Widget | 已弃用 | 问题:Widget* 指针随时可能失效,容易导致悬空指针和内存崩溃。不符合现代 C++ 内存管理实践。 |
WIDGET_OWNS_NATIVE_WIDGET | Widget 拥有 NativeWidget | 已弃用 | 问题:导致平台窗口关闭时出现问题,如关闭动画中断、合成器层过早销毁。无法保证与 OS 的正常交互。 |
我们的任务是创建一个小窗口,于是事情变得很简单,这就是为什么我们填写了
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
views::WidgetDelegate
WidgetDelegate
类定义:ui/views/widget/widget.hWidget
类概述:Chromium UI Platform - Views Overview
这个类非常的庞大,感兴趣的朋友可以自己找找代码阅读,我们只说明感兴趣的东西。WidgetDelegate实际上讲我们对Widget的行为监视回调和行为控制专门设计的一个代理——所有的Widget行为的客制化全部都在views::WidgetDelegate进行设置。
class SimpleDelegate : public views::WidgetDelegate {
public:
SimpleDelegate() = default;
~SimpleDelegate() override = default;
views::View* GetContentsView() override {
if (!contents_) {
contents_ = new SimpleView();
}
return contents_;
}
std::u16string GetWindowTitle() const override {
return u"Simple Demo";
}
void assignedClosure(base::OnceClosure on_close) {
on_close_ = std::move(on_close);
}
void WindowClosing() override {
std::cerr << "Window is Closing!" << std::endl;
if (on_close_) {
std::move(on_close_).Run();
}
}
private:
raw_ptr<views::View> contents_ = nullptr; // raw_ptr笔者有空单独开一个博客聊聊
base::OnceClosure on_close_; // 相当于关闭的回调闭包
};
GetContentsView()
:返回窗口的内容视图。GetWindowTitle()
:返回窗口的标题。assignedClosure(base::OnceClosure on_close)
:设置窗口关闭时的回调函数。WindowClosing()
:在窗口关闭时调用,执行相关清理操作。
views::View
这里才是我们需要提供内容的地方,Delegate告诉Widget你有什么,你需要画什么,有什么控件,views::View就像是其中的数据类,告诉我们画的内容本身
我整理了一下Views的文档说明,是这样的:
View
是views
视图层级结构中的一个矩形区域,是所有视图的基类。
View
是其他View
的容器(没有所谓的叶子视图——这简化了代码,减少了类型转换的麻烦和设计错误)。View
包含了用于尺寸 (bounds
)、布局 (flex
,orientation
等)、子视图绘制和事件分发的基本属性。View
使用一个类似于 XULSprocketLayout
系统的简单盒式布局管理器 (Box Layout Manager
)。如有需要,也可以使用实现LayoutManager
接口的其他布局管理器来对子视图进行布局。- 子类负责实现绘制和存储子类特有的属性及功能。
- 除非另有说明,
views
不是线程安全的,只能在主线程访问。View也有属性(嘿!QProperty!)
旨在通过元数据动态暴露给其他子系统(如开发者工具)的属性必须遵循特定的命名、使用和实现模式。
属性以其基本名称(如 "Frobble",注意首字母大写)开头。设置属性的方法必须命名为
SetXXXX
,获取值的方法为GetXXXX
。例如,Frobble
属性对应SetFrobble
和GetFrobble
。在
SetXXXX
方法中,更新值后,必须调用OnPropertyChanged()
,并将存储位置的地址作为键传入。此外,还可以传入任意PropertyEffects
的组合,以确保调用所需的副作用。每个属性还应该提供一种通过注册回调函数来监听变化的方式。
每个回调都使用现有的
base::Bind
机制,支持各种回调类型:对象方法、普通函数和 Lambda 表达式。对于那些旨在被其他子系统(如开发者工具或声明式布局系统)动态发现的
View
属性,每个View
及其后代都必须包含元数据。这些子系统可以枚举任何给定实例或类的属性,并使用这些信息通过字符串来读写属性值。元数据还可用于从声明式“脚本”实例化和初始化
View
(或其子类)。在每个
View
类的头文件声明中,将宏METADATA_HEADER(<classname>, <view ancestor class>)
放置在初始的private
部分。在对应的
.cc
实现文件中,将以下宏添加到该类所在的命名空间中:
BEGIN_METADATA(<ClassName>)
ADD_PROPERTY_METADATA(<type>, <PropertyName>)
END_METADATA
对于每个属性,在
BEGIN_METADATA
和END_METADATA
宏之间,使用ADD_PROPERTY_METADATA()
添加一个定义。
笔者研究了一下,得到了下面这个demo:
class SimpleView : public views::View {
public:
SimpleView() {
SetLayoutManager(std::make_unique<views::FillLayout>());
auto* label = new views::Label(
u"Hello, simple Views Widget!");
auto fonts = label->GetDefaultFontList().GetFonts();
if(!fonts.empty()) {
gfx::Font default_font = fonts[0];
gfx::Font large_font(default_font.GetFontName(), 20);
label->SetFontList(gfx::FontList(large_font));
}
AddChildView(label);
SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
}
~SimpleView() override = default;
};
这样就足够了!我们实际上创建了一个字体大小为20号的一个label放置到我们views上,默认的是我们的Label会居中显示。同时,我们的Views的背景是白色的!完事。
针对Example的一些调整
我们的Window没有啥其他的资源,所以一些API会被变动一下,也就是ui::ResourceBundle::InitSharedInstanceWithLocale替代了原先的基于资源路径检索的资源加载API。
#include <windows.h>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include "ui/aura/env.h"
#include "base/at_exit.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/i18n/icu_util.h"
#include "base/lazy_instance.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "base/test/test_discardable_memory_allocator.h"
#include "build/build_config.h"
#include "base/test/test_timeouts.h"
#include "mojo/core/embedder/embedder.h"
#include "ui/accessibility/platform/ax_platform_for_test.h"
#include "ui/base/ime/init/input_method_initializer.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_paths.h"
#include "ui/base/win/scoped_ole_initializer.h"
#include "ui/compositor/test/test_context_factories.h"
#include "ui/display/screen.h"
#include "ui/gfx/font_util.h"
#include "ui/gl/init/gl_factory.h"
#include "ui/views/background.h"
#include "ui/views/buildflags.h"
#include "ui/views/controls/label.h"
#include "ui/views/examples/example_base.h"
#include "ui/views/examples/examples_window.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/test/desktop_test_views_delegate.h"
#include "ui/views/view.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/examples/examples_color_mixer.h"
#include "ui/wm/core/wm_state.h"
#include "ui/views/widget/desktop_aura/desktop_screen.h"
// ------------------------
// 自定义 View
// ------------------------
class SimpleView : public views::View {
public:
SimpleView() {
SetLayoutManager(std::make_unique<views::FillLayout>());
auto* label = new views::Label(
u"Hello, simple Views Widget!");
auto fonts = label->GetDefaultFontList().GetFonts();
if(!fonts.empty()) {
gfx::Font default_font = fonts[0];
gfx::Font large_font(default_font.GetFontName(), 20);
label->SetFontList(gfx::FontList(large_font));
}
AddChildView(label);
SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
}
~SimpleView() override = default;
};
class SimpleDelegate : public views::WidgetDelegate {
public:
SimpleDelegate() = default;
~SimpleDelegate() override = default;
views::View* GetContentsView() override {
if (!contents_) {
contents_ = new SimpleView();
}
return contents_;
}
std::u16string GetWindowTitle() const override {
return u"Simple Demo";
}
void assignedClosure(base::OnceClosure on_close) {
on_close_ = std::move(on_close);
}
void WindowClosing() override {
// Call the run_loop's Quit method to end the message loop.
std::cerr << "Window is Closing!" << std::endl;
if (on_close_) {
std::move(on_close_).Run();
}
}
private:
raw_ptr<views::View> contents_ = nullptr;
base::OnceClosure on_close_;
};
base::LazyInstance<base::TestDiscardableMemoryAllocator>::DestructorAtExit
g_discardable_memory_allocator = LAZY_INSTANCE_INITIALIZER;
bool g_initialized_once = false;
int main(int argc, char* argv[]) {
base::CommandLine::Init(argc, argv);
TestTimeouts::Initialize();
base::AtExitManager at_exit;
ui::ScopedOleInitializer ole_initializer;
base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
ui::AXPlatformForTest ax_platform;
// Disabling Direct Composition works around the limitation that
// InProcessContextFactory doesn't work with Direct Composition, causing the
// window to not render. See http://crbug.com/936249.
command_line->AppendSwitch(switches::kDisableDirectComposition);
base::FeatureList::InitInstance(
command_line->GetSwitchValueASCII(switches::kEnableFeatures),
command_line->GetSwitchValueASCII(switches::kDisableFeatures));
if (!g_initialized_once) {
mojo::core::Init();
gl::init::InitializeGLOneOff(
/*gpu_preference=*/gl::GpuPreference::kDefault);
base::i18n::InitializeICU();
ui::RegisterPathProvider();
base::DiscardableMemoryAllocator::SetInstance(
g_discardable_memory_allocator.Pointer());
gfx::InitializeFonts();
g_initialized_once = true;
}
base::test::TaskEnvironment task_environment(
base::test::TaskEnvironment::MainThreadType::UI);
auto context_factories =
std::make_unique<ui::TestContextFactories>(false,
/*output_to_window=*/true);
ui::ResourceBundle::InitSharedInstanceWithLocale(
"en-US", nullptr,
ui::ResourceBundle::LoadResources::DO_NOT_LOAD_COMMON_RESOURCES);
ui::ColorProviderManager::Get().AppendColorProviderInitializer(
base::BindRepeating(&views::examples::AddExamplesColorMixers));
std::unique_ptr<aura::Env> env = aura::Env::CreateInstance();
aura::Env::GetInstance()->set_context_factory(
context_factories->GetContextFactory());
ui::InitializeInputMethodForTesting();
views::DesktopTestViewsDelegate views_delegate;
wm::WMState wm_state;
std::unique_ptr<display::Screen> desktop_screen =
views::CreateDesktopScreen();
// ------------------------
// 创建 Widget
// ------------------------
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
auto* widget = new views::Widget();
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
params.bounds = gfx::Rect(100, 100, 1000, 700);
auto* delegate = new SimpleDelegate();
delegate->assignedClosure(run_loop.QuitClosure());
params.delegate = delegate;
widget->Init(std::move(params));
widget->Show();
auto disable_timeout =
std::make_unique<base::test::ScopedDisableRunLoopTimeout>();
run_loop.Run();
ui::ResourceBundle::CleanupSharedInstance();
ui::ShutdownInputMethod();
env.reset();
return 0;
}
我们编译运行后,就能得到一个简单的窗口,带上居中的Hello, simple Views Widget!了!
Reference
1. views::Widget的介绍说明原文
////////////////////////////////////////////////////////////////////////////////
// Widget class
//
// Encapsulates the platform-specific rendering, event receiving and widget
// management aspects of the UI framework.
//
// Owns a RootView and thus a View hierarchy. Can contain child Widgets.
// Widget is a platform-independent type that communicates with a platform or
// context specific NativeWidget implementation.
//
// All widgets should use ownership = CLIENT_OWNS_WIDGET. The client code that
// creates the widget should hold onto a std::unique_ptr<Widget>. The proper
// way to close the Widget is to reset the unique_ptr.
//
// The Close() and CloseWithReason() methods are problematic because they
// asynchronously close the widget. This means that client code has to handle
// the edge case of: widget is closed, but not destroyed. Use
// MakeCloseSynchronous() to allow the client to intercept these calls
// and reset the unique_ptr. Note that the point of
// MakeCloseSynchronous() is to intercept calls to Close() from code in
// //ui that client code cannot control (such as DialogDelegate). This also
// allows client code to have a single destruction path for widgets, which
// simplifies logic for code that should be written exactly once, such as
// logging. If Client code does not rely on DialogDelegate or similar helpers
// that call Widget::Close(), then MakeCloseSynchronous is unnecessary.
//
// Aside 1: Clients are responsible for handling the case where the parent
// widget is destroyed. There are common helpers like TabDialogManager that
// will do this.
//
// Aside 2: There will always be the edge case of NATIVE_WIDGET destroyed while
// Widget is alive. This is rare and most clients do not need to handle this.
// For clients that do care about this, the best way to detect this right now
// is WidgetObserver::OnWidgetDestroying.
//
// See documentation of MakeCloseSynchronous for an example.
//
// Deprecated but kept for historical context --------------------------------
// A special note on ownership:
//
// Depending on the value of the InitParams' ownership field, the Widget
// either owns or is owned by its NativeWidget:
//
// ownership = NATIVE_WIDGET_OWNS_WIDGET (default)
// The Widget instance is owned by its NativeWidget. When the NativeWidget
// is destroyed (in response to a native destruction message), it deletes
// the Widget from its destructor.
// ownership = WIDGET_OWNS_NATIVE_WIDGET (non-default)
// The Widget instance owns its NativeWidget. This state implies someone
// else wants to control the lifetime of this object. When they destroy
// the Widget it is responsible for destroying the NativeWidget (from its
// destructor). This is often used to place a Widget in a std::unique_ptr<>
// or on the stack in a test.
2 views::View的介绍说明原文
/////////////////////////////////////////////////////////////////////////////
//
// View class
//
// A View is a rectangle within the views View hierarchy. It is the base
// class for all Views.
//
// A View is a container of other Views (there is no such thing as a Leaf
// View - makes code simpler, reduces type conversion headaches, design
// mistakes etc)
//
// The View contains basic properties for sizing (bounds), layout (flex,
// orientation, etc), painting of children and event dispatch.
//
// The View also uses a simple Box Layout Manager similar to XUL's
// SprocketLayout system. Alternative Layout Managers implementing the
// LayoutManager interface can be used to lay out children if required.
//
// It is up to the subclass to implement Painting and storage of subclass -
// specific properties and functionality.
//
// Unless otherwise documented, views is not thread safe and should only be
// accessed from the main thread.
//
// Properties ------------------
//
// Properties which are intended to be dynamically visible through metadata to
// other subsystems, such as dev-tools must adhere to a naming convention,
// usage and implementation patterns.
//
// Properties start with their base name, such as "Frobble" (note the
// capitalization). The method to set the property must be called SetXXXX and
// the method to retrieve the value is called GetXXXX. For the aforementioned
// Frobble property, this would be SetFrobble and GetFrobble.
//
// void SetFrobble(bool is_frobble);
// bool GetFrobble() const;
//
// In the SetXXXX method, after the value storage location has been updated,
// OnPropertyChanged() must be called using the address of the storage
// location as a key. Additionally, any combination of PropertyEffects are
// also passed in. This will ensure that any desired side effects are properly
// invoked.
//
// void View::SetFrobble(bool is_frobble) {
// if (is_frobble == frobble_)
// return;
// frobble_ = is_frobble;
// OnPropertyChanged(&frobble_, kPropertyEffectsPaint);
// }
//
// Each property should also have a way to "listen" to changes by registering
// a callback.
//
// base::CallbackListSubscription AddFrobbleChangedCallback(
// PropertyChangedCallback callback);
//
// Each callback uses the the existing base::Bind mechanisms which allow for
// various kinds of callbacks; object methods, normal functions and lambdas.
//
// Example:
//
// class FrobbleView : public View {
// ...
// private:
// void OnFrobbleChanged();
// base::CallbackListSubscription frobble_changed_subscription_;
// }
//
// ...
// frobble_changed_subscription_ = AddFrobbleChangedCallback(
// base::BindRepeating(&FrobbleView::OnFrobbleChanged,
// base::Unretained(this)));
//
// Example:
//
// void MyView::ValidateFrobbleChanged() {
// bool frobble_changed = false;
// base::CallbackListSubscription subscription =
// frobble_view_->AddFrobbleChangedCallback(
// base::BindRepeating([](bool* frobble_changed_ptr) {
// *frobble_changed_ptr = true;
// }, &frobble_changed));
// frobble_view_->SetFrobble(!frobble_view_->GetFrobble());
// LOG() << frobble_changed ? "Frobble changed" : "Frobble NOT changed!";
// }
//
// Property metadata -----------
//
// For Views that expose properties which are intended to be dynamically
// discoverable by other subsystems, each View and its descendants must
// include metadata. These other subsystems, such as dev tools or a
// declarative layout system, can then enumerate the properties on any given
// instance or class. Using the enumerated information, the actual values of
// the properties can be read or written. This will be done by getting and
// setting the values using string representations. The metadata can also be
// used to instantiate and initialize a View (or descendant) class from a
// declarative "script".
//
// For each View class in their respective header declaration, place the macro
// METADATA_HEADER(<classname>, <view ancestor class>) in the initial private
// section.
//
// In the implementing .cc file, add the following macros to the same
// namespace in which the class resides.
//
// BEGIN_METADATA(View)
// ADD_PROPERTY_METADATA(bool, Frobble)
// END_METADATA
//
// For each property, add a definition using ADD_PROPERTY_METADATA() between
// the begin and end macros.
//
// BEGIN_METADATA(MyView)
// ADD_PROPERTY_METADATA(int, Bobble)
// END_METADATA
/////////////////////////////////////////////////////////////////////////////