17 KiB
About this repo
INSERT YOUR PROJECT DESCRIPTION HERE
Dependencies
This project is built on top of:
Vlpp:Import\Vlpp.hVlppOS:Import\VlppOS.hVlppRegex:Import\VlppRegex.hVlppReflection:Import\VlppReflection.hVlppParser2:Import\VlppGlrParser.h,Import\VlppGlrParserCompiler.hWorkflow:Import\VlppWorkflowLibrary.h,Import\VlppWorkflowCompiler.h,Import\VlppWorkflowRungime.hGacUI:Import\GacUI.h,Import\GacUI.UnitTest.h,Import\GacUI.UnitTest.UI.h,Import\GacUICompiler.h,Import\GacUI.Windows.h
General Instruction
- This project uses C++ 20, you don't need to worry about compatibility with prior version of C++.
- All code should be crossed-platform. In case when OS feature is needed, a Windows version and a Linux version should be prepared in different files, following the
*.Windows.cppand*.Linux.cppnaming convention, and keep them as small as possible. - DO NOT MODIFY any source code in the
Importfolder, they are dependencies. - DO NOT MODIFY any source code in the
Releasefolder, they are generated release files. - You can modify source code in the
SourceandTestfolder. - Use tabs for indentation in C++ source code.
- Header files are guarded with macros instead of
#pragma once. - Use
autoto define variables if it is doable. Useauto&&when the type is big or when it is a collection type. - In header files, do not use
using namespacestatement, full name of types are always required. In a class/struct/union declaration, member names must be aligned in the same column at least in the same public, protected or private session. Please follow this coding style just like other part of the code. - In cpp files, use
using namespacestatement if necessary to prevent from repeating namespace everywhere. - The project only uses a very minimal subset of the standard library. I have substitutions for most of the STL constructions. Always use mine if possible:
- Always use
vintinstead ofint. - Always use
L'x',L"x",wchar_t,const wchar_tandvl::WString, instead ofstd::stringorstd::wstring. - Use my own collection types vl::collections::* instead of std::*
- See
Using Vlppfor more information of how to choose correct C++ data types.
- Always use
Leveraging the Knowledge Base
When making design or coding decisions, you must leverage the knowledge base to make the best choice.
The main entry is Index.md. Find out Accessing the Knowledge Base about how to access the knowledge base.
Index.md is organized in this way:
## Guidance: A general guidance that play a super important part repo-wide.- Each
## Project: A brief description of each project and its purpose.### Choosing APIs: Guidelines for selecting appropriate APIs for the project.### Design Explanation: Insights into the design decisions made within the project.
## Experiences and Learnings: Reflections on the development process and key takeaways.
Project/Choosing APIs
There are multiple categories under Choosing APIs. Each category begins with a short and accurate title #### Category.
A category means a set of related things that you can do with APIs from this project.
Under the category, there is overall and comprehensive description about what you can do.
Under the description, there are bullet points and each item follow the format: - Use CLASS-NAME for blahblahblah (If a function does not belong to a class, you can generate Use FUNCTION-NAME ...).
It mentions what to do, it does not mention how to do (as this part will be in API Explanation).
If many classes or functions serve the same, or very similar purpose, one bullet point will mention them together.
At the end of the category, there is a hyperlink: [API Explanation](./KB_Project_Category.md) (no space between file name, all pascal case).
Project/Design Explanation
There are multiple topics under Design Explanation. Each topic begins with a short and accurate title #### Topic.
A topic means a feature of this project, it will be multiple components combined.
Under the topic, there is overall and comprehensive description about what does this feature do.
Under the description, there are bullet points to provide a little more detail, but do not make it too long. Full details are supposed to be in the document from the hyperlink.
At the end of the topic, there is a hyperlink: [Design Explanation](./KB_Project_Design_Topic.md) (no space between file name, all pascal case).
Experiences and Learnings
(To do ...)
Accessing the Knowledge Base
If you are running in Visual Studio, you will find the KnowledgeBase project in the current solution.
Otherwise, locate the KnowledgeBase project in REPO-ROOT/.github/KnowledgeBase/KnowledgeBase.vcxitems.
REPO-ROOT is the root folder of the repo.
KnowledgeBase.vcxitems is a Visual Studio project file, it is used as a list of all document files in the knowledge base.
In case when a new file is added to the knowledge base, KnowledgeBase.vcxitems must be updated to contain that file.
The entry point is its Index.md file. You need to locate it in KnowledgeBase.vcxitems.
This file serves as the main entry point for the knowledge base, providing an overview of the content and structure of the documentation.
Writing General Unit Test
Test code uses my own unit test framework. Here are some basic rules.
Basic Structure
using namespace vl;
using namespace vl::unittest;
TEST_FILE
{
TEST_CASE(L"TOPIC-NAME")
{
TEST_ASSERT(EXPRESSION-TO-VERIFY);
});
TEST_CATEGORY(L"CATEGORY-NAME")
{
TEST_CASE(L"TOPIC-NAME")
{
TEST_ASSERT(EXPRESSION-TO-VERIFY);
});
});
}
Please refer to the Accessing the Knowledge Base section for more information about advanced features in unit test.
IMPORTANT: Unlike TEST_FILE which ends with }, TEST_CASE and TEST_CATEGORY ends with });.
Writing GacUI Unit Test
Here are some basic rules.
Basic Structure
#include "TestControls.h"
using namespace gacui_unittest_template;
TEST_FILE
{
const auto resourceTestSubject = LR"GacUISrc(
<Resource>
<Instance name="MainWindowResource">
<Instance ref.Class="gacuisrc_unittest::MainWindow">
<Window ref.Name="self" Text="Test Subject" ClientSize="x:480 y:320">
<!-- defines the UI here -->
</Window>
</Instance>
</Instance>
</Resource>
)GacUISrc";
TEST_CATEGORY(L"ClassNameUnderTest")
{
TEST_CASE(L"Topic")
{
GacUIUnitTest_SetGuiMainProxy([](UnitTestRemoteProtocol* protocol, IUnitTestContext*)
{
protocol->OnNextIdleFrame(L"Ready", [=]()
{
// Click this button
});
protocol->OnNextIdleFrame(L"Clicked this button", [=]()
{
// Type that text
});
protocol->OnNextIdleFrame(L"Typed that text", [=]()
{
auto window = GetApplication()->GetMainWindow();
window->Hide();
});
});
GacUIUnitTest_StartFast_WithResourceAsText<darkskin::Theme>(
WString::Unmanaged(L"Controls/Category/ClassNameUnderTest/Topic"),
WString::Unmanaged(L"gacuisrc_unittest::MainWindow"),
resourceTestSubject
);
});
});
}
If multiple test cases are in the same file:
- There can be only one
TEST_FILE. - There can be multiple
TEST_CATEGORYbut usually just one. - There can be multiple
TEST_CASEin aTEST_CATEGORY. - There name will also appear in the arguments to
GacUIUnitTest_StartFast_WithResourceAsTextunless directed.
In GacUIUnitTest_SetGuiMainProxy, there will be multiple protocol->OnNextIdleFrame. Each creates a new frame.
Name of the frame does not say what to do in this frame, but actually what have been done previously.
The code of the last frame is always closing the window.
If there are shared variable that updates in any frame in one TEST_CASE, they must be organized like this:
- Shared variables should be only in
TEST_CASE. - Lambda captures should be exactly like this example,
[&]for the proxy and[&, protocol]for the frame.
TEST_CASE(L"Topic")
{
vint sharedVariable = 0;
GacUIUnitTest_SetGuiMainProxy([&](UnitTestRemoteProtocol* protocol, IUnitTestContext*)
{
protocol->OnNextIdleFrame(L"Ready", [&, protocol]()
{
// Use sharedVariable
});
});
GacUIUnitTest_StartFast_WithResourceAsText<darkskin::Theme>(
WString::Unmanaged(L"Controls/Category/ClassNameUnderTest/Topic"),
WString::Unmanaged(L"gacuisrc_unittest::MainWindow"),
resourceTestSubject
);
});
Accessing objects defined in the XML
Obtain the main window using GetApplication()->GetMainWindow();.
Obtain any named control using FindObjectByName<CLASS-NAME>(MAIN-WINDOW, L"name"), the name reflects in the XML using ref.Name attribute.
Obtain any control with text using FindControlByText<CLASS-NAME>(MAIN-WINDOW, L"text"), the name reflects in the XML using Text attribute.
You should define a variable for any object obtained above.
Performing IO Actions
All methods you can use defines in the UnitTestRemoteProtocol_IOCommands class in Source\UnitTestUtilities\GuiUnitTestProtocol_IOCommands.h.
Call the like protocol->LClick();, there are KeyPress, _Key(Down(Repeat)?|Up), MouseMove, [LMR]((DB)?Click, _[LMR](Down|Up|DBClick).
Mouse actions use the last cursor location. If you offer new location to these functions, it just like calling MouseMove followed by one that is not offered a new location.
Obtain the center of the location using protocol->LocationOf(CONTROL). You should define a variable for a location.
There are 4 more arguments for the function: ratioX, ratioY, offsetX, offsetY. If they are not offered, they will be 0.5, 0.5, 0, 0, which means the center of the control with no offset. ratioX is the horizontal position of the control, ratioY is vertical, offsetX and offsetY adds to them. So if you offer 1.0, 0.0, -2, 3 it means the top right corner but moving 2 pixels left and 3 pixels down.
You can perform mouse, keyboard, typing or any other user actions on the UI. Find examples by yourself for arguments.
Writing GacUI XML
This is the document of GacUI XML: https://gaclib.net/doc/current/gacui/xmlres/home.html
Mapping XML Entity to C++ Entity
Most of XML tags are calling to constructors for classes in the follow folder:
- Source\Controls
- Source\Application
- Source\GraphicsCompositions
- Source\GraphicsElement
All mappings are:
- presentation::controls::Gui*
- presentation::elements::Gui*Element
- presentation::compositions::Gui*Composition
- presentation::compositions::Gui*
- presentation::templates::Gui*
- system::*
- system::reflection::*
- presentation::*
- presentation::Gui*
- presentation::controls::*
- presentation::controls::list::*
- presentation::controls::tree::*
- presentation::elements::*
- presentation::elements::Gui*
- presentation::elements::text::*
- presentation::compositions::*
- presentation::templates::*
- presentation::theme::*
When you see <Button>,
try all mappings and for example presentation::elements::Gui*,
replacing * with the tag and you will get presentation::elements::GuiButton,
it is an existing C++ class!
So <Button> means presentation::controls::GuiButton.
Following the same rule, <Table> would be presentation::compositions::GuiTableComposition and <SolidLabel> would be presentation::elements::GuiSolidLabelElement.
Take this file Test\Resources\Metadata\Reflection64.txt as an index, it collects all valid C++ classes and their members, but written in my own format.
When there is a @FullName on top of a class, it means the full name in C++, the class name will be the full name in XML.
When there is no @FullName but the class name begins with presentation::, the full name in C++ begins with vl::presentation::.
XML in a Single String
To define an empty window, the XML looks like:
<Resource>
<Instance name="MainWindowResource">
<Instance ref.Class="gacuisrc_unittest::MainWindow">
<Window ref.Name="self" Text="GuiRibbonGallery" ClientSize="x:480 y:320">
</Window>
</Instance>
</Instance>
</Resource>
The first <Instance> defines the resource named MainWindowResource, the <Instance> inside is the content. You can also add a script like this:
<Resource>
<Instance name="MainWindowResource">
<Instance ref.Class="gacuisrc_unittest::MainWindow">
<Window ref.Name="self" Text="GuiRibbonGallery" ClientSize="x:480 y:320">
<Label Text="This is a demo"/>
</Window>
</Instance>
</Instance>
<Folder name="Scripts">
<Script name="ViewModelResource">
<Workflow>
<![CDATA[
module viewmodel;
using system::*;
using presentation::*;
namespace demo
{
enum MyCategory
{
Black = 0,
Red = 1,
Lime = 2,
Blue = 3,
White = 4,
}
func ToString(value : DateTime) : string
{
return $"$(value.month)/$(value.day)/$(value.year)";
}
}
]]>
</Workflow>
</Script>
</Folder>
</Resource>
One more Scripts\ViewModelResource is added to the resource, and the content of the new resource is defined by <Workflow>. Code inside <Workflow> will always be Workflow Script instead of C++.
UI Layout
This is the GacUI XML document for UI layout: https://gaclib.net/doc/current/gacui/components/compositions/home.html
To expand a composition to the whole parent client area:
<AnyComposition AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
MinSizeLimitation="LimitToElementAndChildren"for a composition limites its minimum size to the sum of its all children.AlignmentToParent="left:8 top:8 right:8 bottom:8"for a composition stickes itself to the parent's whole client area with 8 pixels on each side. If the number is -1, it means it doesn't stick the the specific parent's client area side. When bothleftandrightis -1, it stick to the left. When bothtopandbottomis -1, it stick to the top. The default value is all -1.
BoundsComposition of a control is its boundary composition. To expand a control to the whole parent client area:
<AnyControl>
<att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
</AnyControl>
<Cell>, <RowSplitter>, <ColumnSplitter>, <StackItem> and <FlowItem> are controlled by its parent composition, no positions or size limits needs to adjusted.
Bounds
<Bounds> is useful to define a space.
Table
The most useful composition is <Table>, it is a grid layout with rows and columns. There are 3 sizing modes for rows and columns:
<_>composeType:MinSize</_>: The size is decided by its content.<_>composeType:Absolute absolute:10</_>: The size is 10.<_>composeType:Percentage percentage:0.5</_>: The size is 50% of all space excludes MinSizes and Absolutes.
The CellPadding property defines the space between cells, default 0. The BorderVisible adds CellPadding arounds the border, default true. Obviously the following two tables are identical.
<Table AlignmentToParent="left:8 top:8 right:8 bottom:8" CellPadding="5" BorderVisible="false"/>
<Table AlignmentToParent="left:3 top:3 right:3 bottom:3" CellPadding="5" BorderVisible="true"/>
In order to make a table expanded to the whole window and placing a button at the center:
- Set rows to Percentage 0.5; MinSize; Percentage 0.5
- Set columns to Percentage 0.5; MinSize; Percentage 0.5
- Put the button to the center cell, which is
Site="row:1 column:1"
We can also list 3 buttons vertically in the top-left corner of the window:
- Set rows to MinSize; MinSize; MinSize; Percentage 1.0
- Set columns to MinSize; Percentage 1.0
- Put 3 button to all MinSize cells, which is
Site="row:0 column:0",Site="row:1 column:0",Site="row:2 column:0"
To make a dialog with big content with OK and Cancel buttons at the bottom-right corner:
- Set rows to Percentage 1.0; MinSize
- Set columns to Percentage 1.0; MinSize; MinSize
- Put the content to the cell that automatically expands to the rest of the space, which is
Site="row:0 column:0" - Put 2 button to all MinSize cells, which is
Site="row:1 column:1",Site="row:1 column:2"
Others
Please read the document of GacUI XML for other compositions: https://gaclib.net/doc/current/gacui/components/compositions/home.html
- Stack/StackItem
- Flow/FlowItem
- SharedSizeRoot/SharedSizeItem
- SideAligned
- PartialView
- etc
Properties
There are two ways to add a property:
<Label Text="This is a demo"/>
<Label>
<att.Text>This is a demo</att.Text>
</Label>
If the object assigned to a property cannot be written as a string, the second way is the only way to do that, for example:
<Label>
</Label>
To access properties in the nested level, the -set binding is required:
<Label>
<att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 botton:0"/>
</Label>
This example changes the property AlignmentToParent to the object in label's BoundsComposition property.
If a property name looks like Name-binding, it means the property Name should interpret the content using the specified way -binding. There are more predefined bindings like -ref, -uri, -eval, -bind, etc.
Events
An event is subscribed like:
<Button>
<ev.Clicked-eval><![CDATA[{
// some code
}]]></ev.Clicked-eval>
</Button>
It means when button's Clicked event happens, execute some code.
Code in an event of in the <Workflow> resource item should be Workflow Script instead of C++.