英文:
How do I get keyboard navigation for the SelectedRow in controlsfx.TableView2?
问题
我使用了来自ControlsFX库的TableView2组件。在一个简单的示例中(见下面的源代码),在将import javafx.scene.control.TableView
更改为import org.controlsfx.control.tableview2.TableView2
后,表格中的ArrowKey导航消失了。
完成 '研究'
我在JavaDocs中看到TableView2
是一个可插入替换,所以我想知道我可以做什么来恢复核心组件的功能。
- 在ControlsFX Sampler 应用程序中也可以观察到导航出现问题。
- 也许不相关:还有一个
SpreadsheetView
示例,它使用了不同外观的CellSelection样式。
问题描述
示例代码来自Oracle教程,我只删除了一些不必要的内容。
尝试鼠标单击表格单元并按箭头下键。表格选择不会向下移动一行,而是整个焦点会移动到TextField。
我使用的是Windows 10 Pro 22H2,JDK corretto-17.0.7,JavaFX 20.0.1和controlsfx-11.1.2
示例代码
如果将 new TableView2()
更改为 new TableView()
,则一切都按预期工作。
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.tableview2.TableView2;
/**
* 从https://docs.oracle.com/javafx/2/ui_controls/table-view.htm 减少。
*/
public class TableView2KeyboardNavigation extends Application
{
static class Main
{
public static void main( String[] args )
{
Application.launch( TableView2KeyboardNavigation.class, args );
}
}
private TableView<Person> table = new TableView2<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person( "Jacob", "Smith" ),
new Person( "Isabella", "Johnson" ),
new Person( "Ethan", "Williams" )
);
@Override
public void start( Stage stage )
{
final var scene = new Scene( new Group() );
stage.setTitle( "TableView2 Sample" );
final var firstNameCol = new TableColumn( "First Name" );
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>( "firstName" ) );
final var lastNameCol = new TableColumn( "Last Name" );
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>( "lastName" ) );
table.setItems( data );
table.getColumns().addAll( firstNameCol, lastNameCol );
final var vbox = new VBox();
vbox.getChildren().addAll( table, new TextField( "Focus lands here after ArrowDown-Key..." ) );
( (Group) scene.getRoot() ).getChildren().addAll( vbox );
stage.setScene( scene );
stage.show();
}
public static class Person
{
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private Person( String fName, String lName )
{
this.firstName = new SimpleStringProperty( fName );
this.lastName = aSimpleStringProperty( lName );
}
public String getFirstName()
{
return firstName.get();
}
public void setFirstName( String fName )
{
firstName.set( fName );
}
public String getLastName()
{
return lastName.get();
}
public void setLastName( String fName )
{
lastName.set( fName );
}
}
}
英文:
I'm using the TableView2 component from the library ControlsFX. In an simple example (see sourcecode below) the ArrowKey-Navigation in the table is gone after changing import javafx.scene.control.TableView
to import org.controlsfx.control.tableview2.TableView2
.
Done 'Research'
I read in the JavaDocs that TableView2
is a drop-in-replacement and so I'm asking what I can do to bring back the functionality of the core-component.
- The broken navigation is observable in the ControlsFX Sampler application as well.
- Maybe unrelated: There is also an
SpreadsheetView
example which is using a different looking CellSelection-style.
Description of the problem
The example code is from the Oracle-Tutorials, I just deleted some unnecessary stuff.
Try mouseclicking a table cell and press the arrow-down key. The TableSelection is not moving one row down, but the whole Focus is traversed to the TextField.
I'm using Windows 10 Pro 22H2, JDK corretto-17.0.7, JavaFX 20.0.1 and controlsfx-11.1.2
Example Code
If you change new TableView2()
to new TableView()
everything works as expected.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.tableview2.TableView2;
/**
* Reduced from https://docs.oracle.com/javafx/2/ui_controls/table-view.htm.
*/
public class TableView2KeyboardNavigation extends Application
{
static class Main
{
public static void main( String[] args )
{
Application.launch( TableView2KeyboardNavigation.class, args );
}
}
private TableView<Person> table = new TableView2<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person( "Jacob", "Smith" ),
new Person( "Isabella", "Johnson" ),
new Person( "Ethan", "Williams" )
);
@Override
public void start( Stage stage )
{
final var scene = new Scene( new Group() );
stage.setTitle( "TableView2 Sample" );
final var firstNameCol = new TableColumn( "First Name" );
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>( "firstName" ) );
final var lastNameCol = new TableColumn( "Last Name" );
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>( "lastName" ) );
table.setItems( data );
table.getColumns().addAll( firstNameCol, lastNameCol );
final var vbox = new VBox();
vbox.getChildren().addAll( table, new TextField( "Focus lands here after ArrowDown-Key..." ) );
( (Group) scene.getRoot() ).getChildren().addAll( vbox );
stage.setScene( scene );
stage.show();
}
public static class Person
{
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private Person( String fName, String lName )
{
this.firstName = new SimpleStringProperty( fName );
this.lastName = new SimpleStringProperty( lName );
}
public String getFirstName()
{
return firstName.get();
}
public void setFirstName( String fName )
{
firstName.set( fName );
}
public String getLastName()
{
return lastName.get();
}
public void setLastName( String fName )
{
lastName.set( fName );
}
}
}
答案1
得分: 2
你观察到的是正确的。确实,TableView2 中已经禁止或移除了 TableView 中默认的行为。
TableView 的一般键盘映射行为在 TableViewBehaviorBase.java 中实现如下:
// 代码部分不翻译
这个行为的实现被设置给了 TableView 在 TableViewSkin 类中。(请注意,它不是在 TableViewSkinBase 中)。
现在来看 TableView2 的实现,TableView2 的皮肤是 TableView2Skin,它也扩展了 TableViewSkinBase。但是在 TableView2Skin 中没有定义 behavior。这就是你看不到相同行为的原因。
简而言之,TableView2 显然不是 TableView 的完全扩展。尽管 TableView2 扩展了 TableView,但 TableView2Skin 并没有扩展 TableViewSkin。所以你将无法获得 TableView 的所有功能。我不确定这是否是一个有意的决定 :).
为了让事情按预期工作,不仅仅是像 tableView.setOnKeyPressed(e -> ... 选择下一行 ...);
这样简单地添加键盘处理程序。这只适用于基本实现。但不要期望与 TableView 完全相同。因为在 TableVieBehavior 中处理了很多内容来执行行选择。
以下是在 TableView 中按键时将执行的代码:
// 代码部分不翻译
由你来决定使用哪个 TableView 更适合你的需求。如果需要使用 TableView2,那么你需要以某种方式将上述代码包含到你的 TableView2 中。基本上,你可以包含下面的代码:
tableView2.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
int index = tableView2.getSelectionModel().getSelectedIndex();
if (e.getCode() == KeyCode.DOWN) {
if (index < tableView2.getItems().size() - 1) {
index++;
}
} else if (e.getCode() == KeyCode.UP) {
if (index > 0) {
index--;
}
}
tableView2.getSelectionModel().select(index);
// 如果不消耗这个事件,焦点将移动到下一个控件。
e.consume();
});
下面是一个快速演示,演示了如何在 TableView2 上使用这个解决方案。
// 代码部分不翻译
希望这能帮助你使用 TableView2。
英文:
What you have observed is correct. It is indeed, that the default behaviour that is available in TableView is supressed or removed in TableView2.
The general key mapping behavior of TableView is implemented in TableViewBehaviorBase.java as below:
addDefaultMapping(tableViewInputMap,
new KeyMapping(TAB, FocusTraversalInputMap::traverseNext),
new KeyMapping(new KeyBinding(TAB).shift(), FocusTraversalInputMap::traversePrevious),
new KeyMapping(HOME, e -> selectFirstRow()),
new KeyMapping(END, e -> selectLastRow()),
new KeyMapping(PAGE_UP, e -> scrollUp()),
new KeyMapping(PAGE_DOWN, e -> scrollDown()),
new KeyMapping(LEFT, e -> { if(isRTL()) selectRightCell(); else selectLeftCell(); }),
new KeyMapping(KP_LEFT,e -> { if(isRTL()) selectRightCell(); else selectLeftCell(); }),
new KeyMapping(RIGHT, e -> { if(isRTL()) selectLeftCell(); else selectRightCell(); }),
new KeyMapping(KP_RIGHT, e -> { if(isRTL()) selectLeftCell(); else selectRightCell(); }),
new KeyMapping(UP, e -> selectPreviousRow()),
new KeyMapping(KP_UP, e -> selectPreviousRow()),
new KeyMapping(DOWN, e -> selectNextRow()),
new KeyMapping(KP_DOWN, e -> selectNextRow()),
new KeyMapping(LEFT, e -> { if(isRTL()) focusTraverseRight(); else focusTraverseLeft(); }),
new KeyMapping(KP_LEFT, e -> { if(isRTL()) focusTraverseRight(); else focusTraverseLeft(); }),
new KeyMapping(RIGHT, e -> { if(isRTL()) focusTraverseLeft(); else focusTraverseRight(); }),
new KeyMapping(KP_RIGHT, e -> { if(isRTL()) focusTraverseLeft(); else focusTraverseRight(); }),
new KeyMapping(UP, FocusTraversalInputMap::traverseUp),
new KeyMapping(KP_UP, FocusTraversalInputMap::traverseUp),
new KeyMapping(DOWN, FocusTraversalInputMap::traverseDown),
new KeyMapping(KP_DOWN, FocusTraversalInputMap::traverseDown),
new KeyMapping(new KeyBinding(HOME).shift(), e -> selectAllToFirstRow()),
new KeyMapping(new KeyBinding(END).shift(), e -> selectAllToLastRow()),
new KeyMapping(new KeyBinding(PAGE_UP).shift(), e -> selectAllPageUp()),
new KeyMapping(new KeyBinding(PAGE_DOWN).shift(), e -> selectAllPageDown()),
new KeyMapping(new KeyBinding(UP).shift(), e -> alsoSelectPrevious()),
new KeyMapping(new KeyBinding(KP_UP).shift(), e -> alsoSelectPrevious()),
new KeyMapping(new KeyBinding(DOWN).shift(), e -> alsoSelectNext()),
new KeyMapping(new KeyBinding(KP_DOWN).shift(), e -> alsoSelectNext()),
new KeyMapping(new KeyBinding(SPACE).shift(), e -> selectAllToFocus(false)),
new KeyMapping(new KeyBinding(SPACE).shortcut().shift(), e -> selectAllToFocus(true)),
new KeyMapping(new KeyBinding(LEFT).shift(), e -> { if(isRTL()) alsoSelectRightCell(); else alsoSelectLeftCell(); }),
new KeyMapping(new KeyBinding(KP_LEFT).shift(), e -> { if(isRTL()) alsoSelectRightCell(); else alsoSelectLeftCell(); }),
new KeyMapping(new KeyBinding(RIGHT).shift(), e -> { if(isRTL()) alsoSelectLeftCell(); else alsoSelectRightCell(); }),
new KeyMapping(new KeyBinding(KP_RIGHT).shift(), e -> { if(isRTL()) alsoSelectLeftCell(); else alsoSelectRightCell(); }),
new KeyMapping(new KeyBinding(UP).shortcut(), e -> focusPreviousRow()),
new KeyMapping(new KeyBinding(DOWN).shortcut(), e -> focusNextRow()),
new KeyMapping(new KeyBinding(RIGHT).shortcut(), e -> { if(isRTL()) focusLeftCell(); else focusRightCell(); }),
new KeyMapping(new KeyBinding(KP_RIGHT).shortcut(), e -> { if(isRTL()) focusLeftCell(); else focusRightCell(); }),
new KeyMapping(new KeyBinding(LEFT).shortcut(), e -> { if(isRTL()) focusRightCell(); else focusLeftCell(); }),
new KeyMapping(new KeyBinding(KP_LEFT).shortcut(), e -> { if(isRTL()) focusRightCell(); else focusLeftCell(); }),
new KeyMapping(new KeyBinding(A).shortcut(), e -> selectAll()),
new KeyMapping(new KeyBinding(HOME).shortcut(), e -> focusFirstRow()),
new KeyMapping(new KeyBinding(END).shortcut(), e -> focusLastRow()),
new KeyMapping(new KeyBinding(PAGE_UP).shortcut(), e -> focusPageUp()),
new KeyMapping(new KeyBinding(PAGE_DOWN).shortcut(), e -> focusPageDown()),
new KeyMapping(new KeyBinding(UP).shortcut().shift(), e -> discontinuousSelectPreviousRow()),
new KeyMapping(new KeyBinding(DOWN).shortcut().shift(), e -> discontinuousSelectNextRow()),
new KeyMapping(new KeyBinding(LEFT).shortcut().shift(), e -> { if(isRTL()) discontinuousSelectNextColumn(); else discontinuousSelectPreviousColumn(); }),
new KeyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> { if(isRTL()) discontinuousSelectPreviousColumn(); else discontinuousSelectNextColumn(); }),
new KeyMapping(new KeyBinding(PAGE_UP).shortcut().shift(), e -> discontinuousSelectPageUp()),
new KeyMapping(new KeyBinding(PAGE_DOWN).shortcut().shift(), e -> discontinuousSelectPageDown()),
new KeyMapping(new KeyBinding(HOME).shortcut().shift(), e -> discontinuousSelectAllToFirstRow()),
new KeyMapping(new KeyBinding(END).shortcut().shift(), e -> discontinuousSelectAllToLastRow()),
enterKeyActivateMapping = new KeyMapping(ENTER, this::activate),
new KeyMapping(SPACE, this::activate),
new KeyMapping(F2, this::activate),
escapeKeyCancelEditMapping = new KeyMapping(ESCAPE, this::cancelEdit),
new InputMap.MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed)
);
And this behavior implementation is set to TableView in TableViewSkin class. (Please note that it is not in TableViewSkinBase).
Now coming to TableView2 implementation, the skin of TableView2 is TableView2Skin which also extends TableViewSkinBase. But there is no behavior defined in this TableView2Skin. This is the reason why you cannot see the same behavior.
In short, TableView2 is definitely not a complete extension to TableView. Though TableView2 extends TableView, the TableView2Skin is not extending TableViewSkin. So you will not get all the features of TableView. And I am not sure whether this is an intended decision or not :).
And to get the things work as expected, it is not as easy as just adding a key handler to TableView like tableView.setOnKeyPressed(e->...select next row...);
. This will work for basic implementation. But don't expect to be as same as TableView. Because in TableVieBehavior a lot of stuff is handled to do the row selection.
Below is the code that will be executed, when a key press is done in TableView.
new KeyMapping(DOWN, e -> selectNextRow()),
protected void selectNextRow() {
selectCell(1, 0);
if (onSelectNextRow != null) onSelectNextRow.run();
}
protected void selectCell(int rowDiff, int columnDiff) {
TableSelectionModel sm = getSelectionModel();
if (sm == null) return;
TableFocusModel fm = getFocusModel();
if (fm == null) return;
TablePositionBase<TC> focusedCell = getFocusedCell();
int currentRow = focusedCell.getRow();
int currentColumn = getVisibleLeafIndex(focusedCell.getTableColumn());
if (rowDiff > 0 && currentRow >= getItemCount() - 1) return;
else if (columnDiff < 0 && currentColumn <= 0) return;
else if (columnDiff > 0 && currentColumn >= getVisibleLeafColumns().size() - 1) return;
else if (columnDiff > 0 && currentColumn == -1) return;
TableColumnBase tc = focusedCell.getTableColumn();
tc = getColumn(tc, columnDiff);
//JDK-8222214: Moved this "if" here because the first row might be focused and not selected, so
// this makes sure it gets selected when the users presses UP. If not it ends calling
// VirtualFlow.scrollTo(-1) at and the content of the TableView disappears.
int row = (currentRow <= 0 && rowDiff <= 0) ? 0 : focusedCell.getRow() + rowDiff;
sm.clearAndSelect(row, tc);
setAnchor(row, tc);
}
protected void setAnchor(int row, TableColumnBase col) {
setAnchor(row == -1 && col == null ? null : getTablePosition(row, col));
}
public static <T> void setAnchor(Control control, T anchor, boolean isDefaultAnchor) {
if (control == null) return;
if (anchor == null) {
removeAnchor(control);
} else {
control.getProperties().put(ANCHOR_PROPERTY_KEY, anchor);
control.getProperties().put(IS_DEFAULT_ANCHOR_KEY, isDefaultAnchor);
}
}
It is up to you to take the decision of which TableView to use for your needs. If you need TableView2, then you need to somehow include the above code to your TableView2. At a basic level you can include the below one:
tableView2.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
int index = tableView2.getSelectionModel().getSelectedIndex();
if (e.getCode() == KeyCode.DOWN) {
if (index < tableView2.getItems().size() - 1) {
index++;
}
} else if (e.getCode() == KeyCode.UP) {
if (index > 0) {
index--;
}
}
tableView2.getSelectionModel().select(index);
// If I don't consume this event, the focus will move away to next control.
e.consume();
});
Below is a quick demo that demonstrates this solution on TableView2.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.controlsfx.control.tableview2.TableView2;
import java.util.stream.Stream;
public class TableView2Demo extends Application {
@Override
public void start(final Stage stage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Harry", "John", "LS"));
persons.add(new Person("Mary", "King", "MS"));
persons.add(new Person("Don", "Bon", "CAT"));
VBox root = new VBox();
root.setSpacing(5);
root.setPadding(new Insets(5));
TableView<Person> tableView1 = new TableView<>();
tableView1.setId("TableView");
TableView2<Person> tableView2 = new TableView2<>();
tableView2.setId("TableView2");
Stream.of(tableView1, tableView2).forEach(tableView -> {
TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
TableColumn<Person, String> cityCol = new TableColumn<>("City");
cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
fnCol.setPrefWidth(150);
lnCol.setPrefWidth(100);
cityCol.setPrefWidth(200);
tableView.getColumns().addAll(fnCol, lnCol, cityCol);
tableView.setItems(persons);
Label lbl = new Label(tableView.getId());
lbl.setMinHeight(30);
lbl.setAlignment(Pos.BOTTOM_LEFT);
lbl.setStyle("-fx-font-weight:bold;-fx-font-size:16px;");
root.getChildren().addAll(lbl, tableView);
VBox.setVgrow(tableView, Priority.ALWAYS);
});
// You need to add a handler to get focused when mouse clicked.
tableView2.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> tableView2.requestFocus());
// And another handler for row selection
tableView2.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
int index = tableView2.getSelectionModel().getSelectedIndex();
if (e.getCode() == KeyCode.DOWN) {
if (index < tableView2.getItems().size() - 1) {
index++;
}
} else if (e.getCode() == KeyCode.UP) {
if (index > 0) {
index--;
}
}
tableView2.getSelectionModel().select(index);
// If I don't consume this event, the focus will move away to next control.
e.consume();
});
Scene scene = new Scene(root, 500, 400);
stage.setScene(scene);
stage.setTitle("TableView2 Demo");
stage.show();
}
class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private StringProperty city = new SimpleStringProperty();
public Person(String fn, String ln, String cty) {
setFirstName(fn);
setLastName(ln);
setCity(cty);
}
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public String getCity() {
return city.get();
}
public StringProperty cityProperty() {
return city;
}
public void setCity(String city) {
this.city.set(city);
}
}
}
答案2
得分: 0
没有所谓的“插拔式替代品”...那只是个童话。
你所做的是创建了一个表视图,填充了一些内容,然后将它放到了窗口的场景容器中。没有别的。
要处理特定的键盘事件,你需要使用 table.setOnKeyPressed(some_event_handler)
来分配一个特定的 javafx.event.EventHandler
,它会执行你告诉它要做的事情。
我建议你在调试模式下使用一个 javafx.scene.control.TableView
实例,以确保它实际上能正常工作... 找出那个 onKeyPressed/Released
是在哪里声明的 (也就是... 一旦监听器启动,也就是处理程序启动的代码块) ... 然后在你声明 tableView2
实例之后,将那段代码大致复制一遍。
代码应该大致如下:
tableView.setOnKeyReleased((KeyEvent t)-> { // 或者 key pressed ... 或者 key down...
KeyCode key=t.getCode();
if (key==KeyCode.DOWN){
int pos=tableView.getSelectionModel().getSelectedIndex();
System.out.println("松开键盘时所在位置 :: "+ pos);
}
// 添加类似的 UP ... 或者左右 的部分
});
英文:
There's no such thing as a "drop-in replacement" ... it's a fairy tale.
what you did there is create a table view, fill it with some stuff, and plop it in a scene container of a window. nothing else.
to handle specific key listener stuff, you need to use table.setOnKeyPressed(some_event_handler)
in order to assign a specific javafx.event.EventHandler
which would do what you tell it to.
i advise you to use a javafx.scene.control.TableView
instance in debug mode in whatever case you have going on which actually works... figure out where that onKeyPressed/Released
thing is being declared (as in ... where the code block that gets executed once the listener kicks in a.k.a. handler) ... and go from there ... then duplicate that block of code more or less after you declare your instance of tableView2
it should look something like
tableView.setOnKeyReleased((KeyEvent t)-> { // or key pressed ... or key down...
KeyCode key=t.getCode();
if (key==KeyCode.DOWN){
int pos=tableView.getSelectionModel().getSelectedIndex();
System.out.println("key released while at position :: "+ pos);
}
// add similar for UP ... maybe left or right
});
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论