简体   繁体   English

JavaFX切换BorderPane的中心:按钮只能使用一次

[英]JavaFX Switching BorderPane's Center: Buttons only work once

I'm a tad confused with JavaFX at the minute. 我现在有点与JavaFX混淆。 Basically, when I run my code, I can only click on a button on the side bar of the application once, then it will swap the center pane for the one that I want displayed. 基本上,运行代码时,我只能单击一次应用程序侧栏上的按钮,然后它将中心窗格替换为我要显示的窗格。 After that though, it appears that the ActionEvent does not trigger... I've tried reattaching them after handling but it doesn't work, and I have no idea whats wrong. 但是,在那之后,似乎ActionEvent不会触发...我尝试在处理后重新附加它们,但是它不起作用,而且我也不知道怎么了。

I've spent 2 days trying to crack this, and I'm sure it's something so annoyingly simple. 我花了2天的时间来解决这个问题,而且我敢肯定这是如此简单。

Controller: 控制器:

@FXML private Button fooButton, barButton;
@FXML private Pane fooPane, barPane;
@FXML private BorderPane mainWindow;
@FXML private TabPane tabPane;
@FXML private VBox buttonBar;
@FXML private AnchorPane centerAP;
private HashMap<Button, Pane> buttonsPaneHMap = new HashMap<>(); //storing the data in a HashMap to create a link between buttons and their panes.

@Override
public void initialize(URL arg0, ResourceBundle arg1) {
    putNodesInHashmap();
    assertControlsExist();
    mainWindow.setCenter(welcomePane);
    setOnActions(buttonsPaneHMap);  
}

public final void handleButton(ActionEvent event) throws IOException {
    Node newCenter = new AnchorPane();

    if (event.getSource() ==  fooButton){
        newCenter = FXMLLoader.load(getClass().getResource("/FXML/fooPane.fxml"));
    }   
    if (event.getSource() ==  barButton){
        newCenter = FXMLLoader.load(getClass().getResource("/FXML/barPane.fxml"));
    }               

    try{
        this.mainWindow.setCenter(newCenter); 
     }
     catch (NullPointerException e){
         e.printStackTrace();
     }   
 }

public final void setOnActions(HashMap<Button, Pane> hMap){
    for (Button button : hMap.keySet()){
        ((ButtonBase) button).setOnAction(arg0 -> {
            try {
                handleButton(arg0);
            } 
            catch (Exception e) {
                e.printStackTrace();
            }
        });
    }       
}
public final void putNodesInHashMap(){
     buttonsPaneHMap.put(fooButton, fooPane);
     buttonsPaneHMap.put(barButton, barPane);

}

FXML XML文件

<?xml version="1.0" encoding="UTF-8"?>

<?import java.net.URL?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<BorderPane fx:id="mainWindow" prefHeight="461.0" prefWidth="760.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="guiControllers.MainController">
   <top>
      <Pane id="body" prefHeight="96.0" prefWidth="658.0" style="-fx-background-color: #243242; -fx-border-color: #0E141B; -fx-border-radius: 3;" stylesheets="@application.css" BorderPane.alignment="CENTER">
         <children>
            <Label layoutX="103.0" layoutY="25.0" prefHeight="48.0" prefWidth="394.0" text="Title Here" textFill="WHITE">
               <font>
                  <Font name="Calibri Bold" size="41.0" />
               </font>
            </Label>
            <ImageView fitHeight="55.0" fitWidth="61.0" layoutX="25.0" layoutY="20.0" pickOnBounds="true" preserveRatio="true">
               <image>
                  <Image url="@../Res/mhlogo.png" />
               </image>
            </ImageView>
         </children>
      </Pane>
   </top>
   <left>
      <VBox id="buttonBar" fx:id="buttonBar" alignment="TOP_CENTER" prefHeight="365.0" prefWidth="168.0" style="-fx-background-color: #2E4055; -fx-border-radius: 3; -fx-border-color: #0E141B;" BorderPane.alignment="CENTER">
         <children>
            <Pane prefHeight="31.0" prefWidth="98.0">
               <children>
                  <Pane layoutX="-1.0" layoutY="-2.0" prefHeight="33.0" prefWidth="169.0" style="-fx-background-color: #565656; -fx-border-color: #000000; -fx-border-radius: 20; -fx-background-radius: 20;">
                     <children>
                        <ImageView fitHeight="19.0" fitWidth="18.0" layoutX="7.0" layoutY="7.0" pickOnBounds="true" preserveRatio="true">
                           <image>
                              <Image url="@../Res/magnifying-glass.png" />
                           </image>
                        </ImageView>
                        <TextField layoutX="29.0" layoutY="2.0" prefHeight="0.0" prefWidth="134.0" style="-fx-border-radius: 1; -fx-border-color: #111111; -fx-border-width: 2; -fx-background-color: #FFFFFF; -fx-background-radius: 20; -fx-border-radius: 20;" styleClass="stop-color-leaking" stylesheets="@../cSS/application.css" />
                     </children>
                  </Pane>
               </children>
            </Pane>
            <Button id="fooButton" fx:id="fooButton" mnemonicParsing="false" onAction="#handleButton" prefHeight="31.0" prefWidth="171.0" style="-fx-background-color: #CDCDCD; -fx-border-color: #0E141B; -fx-border-radius: 3;" text="foo" />
            <Button id="barButton" fx:id="barButton" mnemonicParsing="false" onAction="#handleButton" prefHeight="31.0" prefWidth="202.0" style="-fx-background-color: #CDCDCD; -fx-border-color: #0E141B; -fx-border-radius: 3;" text="bar" />

               <children>
                  <ImageView id="settingsButton" fitHeight="38.0" fitWidth="48.0" layoutX="64.0" layoutY="130.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../Res/settings.png" />
                     </image>
                  </ImageView>
               </children>
            </AnchorPane>
         </children>
      </VBox>
   </left>
   <right>
      <TabPane id="tabPane" fx:id="tabPane" focusTraversable="false" prefHeight="365.0" prefWidth="166.0" rotateGraphic="true" style="-fx-background-color: # #414760;" styleClass="tab-header-background" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
        <tabs>
          <Tab fx:id="notesTab" text="Notes">
            <content>
              <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="336.0" prefWidth="216.0" style="-fx-border-color: #414760; -fx-background-radius: 3;" styleClass="tab-header-background" stylesheets="@../application/CSS/application.css" />
            </content>
          </Tab>
          <Tab fx:id="diagramTab" closable="false" text="Diagram" />
        </tabs>
         <cursor>
            <Cursor fx:constant="DEFAULT" />
         </cursor>
         <stylesheets>
            <URL value="@application.css" />
            <URL value="@../application/CSS/application.css" />
         </stylesheets>
      </TabPane>
   </right>
   <center>
      <AnchorPane fx:id="centerAP" style="-fx-background-color: #414760;" BorderPane.alignment="CENTER">
         <children>
            <VBox fx:id="welcomePane" prefHeight="304.0" prefWidth="391.0" style="-fx-background-color: #414760;">
               <children>
                  <Pane fx:id="welcomePane" prefHeight="313.0" prefWidth="428.0" style="-fx-background-color: #414760;">
                     <children>
                        <ImageView fitHeight="183.0" fitWidth="296.0" layoutX="14.0" layoutY="65.0" pickOnBounds="true" preserveRatio="true">
                           <image>
                              <Image url="@../Res/welcomepane.png" />
                           </image>
                        </ImageView>
                        <Label layoutX="141.0" layoutY="14.0" text="Welcome" textFill="WHITE">
                           <font>
                              <Font name="Calibri Bold" size="33.0" />
                           </font>
                        </Label>
                        <Label layoutX="82.0" layoutY="53.0" prefHeight="68.0" prefWidth="346.0" text="To start, please select an" textFill="WHITE" textOverrun="CLIP">
                           <font>
                              <Font name="Calibri" size="24.0" />
                           </font>
                        </Label>
                        <Label layoutX="82.0" layoutY="80.0" prefHeight="68.0" prefWidth="346.0" text="option from the left." textFill="WHITE" textOverrun="CLIP">
                           <font>
                              <Font name="Calibri" size="24.0" />
                           </font>
                        </Label>
                     </children>
                  </Pane>
               </children>
            </VBox>
         </children>
      </AnchorPane>
   </center>
</BorderPane>

As far as I can tell all objects are injected correctly from the FXML, but once the center panel has switched, the side buttons no longer function (Though I can click any initially and it will load. 据我所知,所有对象都是从FXML正确注入的,但是一旦中央面板切换,侧面按钮就不再起作用(尽管我可以单击任何初始按钮,它将加载。

Footnote: The code above is slightly cut down and changed for readability. 脚注:上面的代码已被稍微删减并更改了可读性。

I took an idea from Android's playbook. 我从Android的剧本中得到了一个主意。 If you know how to get to the node's parent and you know the node's fx:id, you can use this approach. 如果您知道如何到达节点的父节点,并且知道节点的fx:id,则可以使用此方法。

The full code loads different panes into the center of a Scene depending on which button is press. 完整的代码根据按下哪个按钮将不同的窗格加载到场景的中心。 The code below is a sample that show how one pane is loaded. 下面的代码是一个示例,显示了如何加载一个窗格。 You can get any node using this idea if you know the node's parent, the node's fx:id and the node's type for casting. 如果您知道节点的父节点,节点的fx:id以及该节点的类型,则可以使用此想法获得任何节点。

Controller code 控制器代码

private void showSetupAccountScreen()
{
    try 
    {
        spCenterDisplay.getChildren().remove(0);//remove old display            
        BorderPane root = FXMLLoader.load(getClass().getResource("SubSetupAccount.fxml"));
        spCenterDisplay.getChildren().add(root);//add new display
        GridPane tempDisplay = (GridPane)root.getChildren().get(1);//get Parent of the nodes I will be using in this controller
        loadQWERTYKeyboard();            

        TextField tfFirstName = (TextField)findNodeByID("tfFirstName", tempDisplay.getChildren());
        TextField tfLastName = (TextField)findNodeByID("tfLastName", tempDisplay.getChildren());
        TextField tfStreetAddress = (TextField)findNodeByID("tfStreetAddress", tempDisplay.getChildren());
        TextField tfCity = (TextField)findNodeByID("tfCity", tempDisplay.getChildren());
        TextField tfState = (TextField)findNodeByID("tfState", tempDisplay.getChildren());
        TextField tfZip = (TextField)findNodeByID("tfZip", tempDisplay.getChildren());
        TextField tfInitialDepositChecking = (TextField)findNodeByID("tfInitialDepositChecking", tempDisplay.getChildren());
        TextField tfInitialDepositSavings  = (TextField)findNodeByID("tfInitialDepositSavings", tempDisplay.getChildren());
        ChoiceBox cbChecking  = (ChoiceBox)findNodeByID("cbChecking", tempDisplay.getChildren());
        cbChecking.getItems().addAll("No", "Yes");
        cbChecking.setValue("No");
        ChoiceBox cbSavings  = (ChoiceBox)findNodeByID("cbSavings", tempDisplay.getChildren());
        cbSavings.getItems().addAll("No", "Yes");
        cbSavings.setValue("No");            

        if(true)//come back and check to make sure all info is in textfields
        {
            btnLeftOne.setOnAction((event) -> {

                boolean createChecking = cbChecking.getValue().equals("Yes");
                boolean createSavings = cbSavings.getValue().equals("Yes");

                dbh.createNewAccount(tfFirstName.getText(), tfLastName.getText(), tfStreetAddress.getText(), tfCity.getText(), 
                                     tfState.getText(), tfZip.getText(), createChecking, Double.parseDouble(tfInitialDepositChecking.getText()),
                                     createSavings, Double.parseDouble(tfInitialDepositSavings.getText()));
            });
        }
        else
        {
            //create Alert 
        }

        btnRightOne.setOnAction((event) -> {
            cancelAccountCreation();
        });

        btnLeftTwo.setOnAction(null);
        btnLeftThree.setOnAction(null);
        btnLeftFour.setOnAction(null);
        btnRightTwo.setOnAction(null);
        btnRightThree.setOnAction(null);
        btnRightFour.setOnAction(null);
    }
    catch (IOException ex) 
    {
        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private void loadQWERTYKeyboard()
{
    try 
    {
        AnchorPane keyboardRoot = FXMLLoader.load(getClass().getResource("KeyboardQWERTY.fxml"));
        System.out.println(keyboardRoot.getId());
        spBottomDisplay.getChildren().add(keyboardRoot);

        GridPane tempKeyboard = (GridPane)keyboardRoot.getChildren().get(0);

        tempKeyboard.getChildren().stream().filter((tempNode)
                -> (tempNode instanceof Button)).map((
                        tempNode) -> (Button) tempNode).forEachOrdered((tempButton) -> {
                            buttons.put(tempButton.getText().toLowerCase(), tempButton);
                        });

        apMain.setOnKeyPressed((event) -> {
            Button tempButton = buttons.get(event.getText());
            if (tempButton != null) {
                tempButton.arm();
                tempButton.setStyle("-fx-background-color: blue");
            }
            else if (event.getCode().equals(KeyCode.ENTER)) {
                tempButton = buttons.get("enter");
                tempButton.arm();
                tempButton.setStyle("-fx-background-color: blue");
            }
            else if (event.getCode().equals(KeyCode.BACK_SPACE)) {
                tempButton = buttons.get("backspace");
                tempButton.arm();
                tempButton.setStyle("-fx-background-color: blue");
            }
            else if (event.getCode().equals(KeyCode.SPACE)) {
                tempButton = buttons.get("space");
                tempButton.arm();
                tempButton.setStyle("-fx-background-color: blue");
            }
        });

        apMain.setOnKeyReleased((event) -> {
            System.out.println();
            Button tempButton = buttons.get(event.getText());
            System.out.println("Released key text: " + event.getText());
            System.out.println("Released key code: " + event.getCode());

            if (tempButton != null) {
                tempButton.disarm();
                tempButton.setStyle("");
            }
            else if (event.getCode().equals(KeyCode.ENTER)) {
                tempButton = buttons.get("enter");
                tempButton.disarm();
                tempButton.setStyle("");
            }
            else if (event.getCode().equals(KeyCode.BACK_SPACE)) {
                tempButton = buttons.get("backspace");
                tempButton.disarm();
                tempButton.setStyle("");
            }
            else if (event.getCode().equals(KeyCode.SPACE)) {
                tempButton = buttons.get("space");
                tempButton.disarm();
                tempButton.setStyle("");
            }
        });
    }
    catch (IOException ex) 
    {
        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private Node findNodeByID(String id, ObservableList<Node> observableList)
{
    for(Node node : observableList)
    {
        if(node.getId().equals(id))
        {
            System.out.println("node found!");
            return node;
        }
        else
        {
            System.out.println("node not found yet!");
        }
    }

    return null;
}

In this snippet of code I use two different approaches. 在这段代码中,我使用两种不同的方法。 In the loadQWERTYKeyboard methods is one approach. 在loadQWERTYKeyboard方法中是一种方法。 In the findNodeByID is the second approach. 在findNodeByID中是第二种方法。 The full code is found here . 完整的代码在这里找到。 Working but project not complete. 工作正常,但项目尚未完成。

This answer is similar to the code you posted. 这个答案类似于您发布的代码。 This answer uses the same two ideas talked about in the original answer. 该答案使用原始答案中提到的两个相同的想法。

Main 主要

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication75 extends Application
{

    @Override
    public void start(Stage stage) throws Exception
    {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
    {
        launch(args);
    }

}

FXML XML文件

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane id="AnchorPane" prefHeight="449.0" prefWidth="564.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="javafxapplication75.FXMLDocumentController">
    <children>
        <Button fx:id="btnMainBar" layoutX="76.0" layoutY="391.0" onAction="#handleButtonAction" text="Load Bar" />
        <Label fx:id="label" layoutX="126" layoutY="120" minHeight="16" minWidth="69" />
      <Button fx:id="btnMainFoo" layoutX="419.0" layoutY="391.0" mnemonicParsing="false" onAction="#handleButtonAction" text="Load Foo" />
      <AnchorPane fx:id="apMain" layoutX="161.0" layoutY="88.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="100.0" AnchorPane.leftAnchor="50.0" AnchorPane.rightAnchor="50.0" AnchorPane.topAnchor="25.0" />
    </children>
</AnchorPane>

Controller 控制者

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;

/**
 *
 * @author blj0011
 */
public class FXMLDocumentController implements Initializable
{    
    @FXML AnchorPane apMain;//This pane will be used to display the other two panes depending on which button is pressed.

    //Foo Pane children nodes
    Button btnFooOne, btnFooTwo;
    TextField tfFooOne, tfFooTwo;
    Label lblFoo;

    //Bar Pane children nodes
    Button btnBar;
    TextField tfBar;
    Label lblBar;

    @FXML
    private void handleButtonAction(ActionEvent event)
    {
        if(((Button)event.getSource()).getId().equals("btnMainBar"))
        {
            loadBarPane();
        }
        else if(((Button)event.getSource()).getId().equals("btnMainFoo"))
        {
            loadFooPane();
        }
    }

    @Override
    public void initialize(URL url, ResourceBundle rb)
    {
        // TODO
    }   

    //This approach uses the findNodeByID method.
    private void loadFooPane()
    {
        try 
        {
            if(apMain.getChildren().size() > 0)
            {
                apMain.getChildren().remove(0);//if a node is loaded into apMain, remove the node.
            }

            Pane root = FXMLLoader.load(getClass().getResource("Foo.fxml"));
            apMain.getChildren().add(root);//Add Foo Pane to apMain

            //Get Foo Pane's children nodes
            tfFooOne = (TextField)findNodeByID("tfFooOne", root.getChildren());
            tfFooTwo = (TextField)findNodeByID("tfFooTwo", root.getChildren());
            btnFooOne = (Button)findNodeByID("btnFooOne", root.getChildren());
            btnFooTwo = (Button)findNodeByID("btnFooTwo", root.getChildren());
            lblFoo = (Label)findNodeByID("lblFoo", root.getChildren());

            //Set Listeners

            tfFooOne.textProperty().addListener((obserValue, oldValue, newValue) -> {
                lblFoo.setText("new value: " + newValue);
            });

            tfFooTwo.textProperty().addListener((obserValue, oldValue, newValue) -> {
                lblFoo.setText("new value: " + newValue);
            });

            btnFooOne.setOnAction((event) -> {lblFoo.setText("You pressed btnFooOne");});
            btnFooTwo.setOnAction((event) -> {lblFoo.setText("You pressed btnFooTwo");});

        }
        catch (IOException ex) 
        {
            Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void loadBarPane()
    {
        try
        {
            //This way is more complicate if you have more than one of the same type of node
            if(apMain.getChildren().size() > 0)
            {
                apMain.getChildren().remove(0);//if a node is loaded into apMain, remove the node.
            }
            Pane root = FXMLLoader.load(getClass().getResource("Bar.fxml"));
            apMain.getChildren().add(root);//Add Foo Pane to apMain

            //Get Bar Pane's children nodes
            for(Node node : root.getChildren())
            {
                if(node instanceof Button)
                {
                    btnBar = (Button)node;
                }
                else if(node instanceof TextField)
                {
                    tfBar = (TextField)node;
                }
                else if(node instanceof Label)
                {
                    lblBar = (Label)node;
                }
            }

            //Set listeners
            tfBar.textProperty().addListener((obserValue, oldValue, newValue) -> {
                lblBar.setText("new value: " + newValue);
            });

            btnBar.setOnAction((event) -> {lblBar.setText("You pressed btnFooOne");});
        }
        catch (IOException ex)
        {
            Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private Node findNodeByID(String id, ObservableList<Node> observableList)
    {
        for(Node node : observableList)
        {
            if(node.getId().equals(id))
            {
                System.out.println("node found!");
                return node;
            }
            else
            {
                System.out.println("node not found yet!");
            }
        }

        return null;
    }


}

Foo FXML Foo FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.Pane?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="324.0" prefWidth="464.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Button fx:id="btnFooOne" layoutX="84.0" layoutY="209.0" mnemonicParsing="false" text="Button" />
      <Button fx:id="btnFooTwo" layoutX="329.0" layoutY="209.0" mnemonicParsing="false" text="Button" />
      <TextField fx:id="tfFooOne" layoutX="36.0" layoutY="162.0" />
      <TextField fx:id="tfFooTwo" layoutX="281.0" layoutY="162.0" />
      <Label fx:id="lblFoo" layoutX="165.0" layoutY="39.0" text="You just loaded Foo Pane" />
   </children>
</Pane>

Bar FXML 酒吧FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.Pane?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="324.0" prefWidth="464.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Button fx:id="btnBar" layoutX="206.0" layoutY="186.0" mnemonicParsing="false" text="Button" />
      <TextField fx:id="tfBar" layoutX="158.0" layoutY="130.0" />
      <Label fx:id="lblBar" layoutX="160.0" layoutY="65.0" text="You just loaded Bar Pane" />
   </children>
</Pane>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM