用Swing 組件編寫的記事本

 原文地址:http://www.blogjava.net/qnjian/archive/2006/04/15/41183.html

     以下是我用Swing 組件編寫的記事本,功能是模仿微軟的,使用了觀感,自我覺得界面比Win的記事本更爲好看(臭屁一下吧)。除了沒有做字體選擇之外,其他功能基本都有了吧。

/**
 *Author:
Zhang Zhijian 
 *Mail: [email protected]
 *Created  on 2005-5-22
 * 聲明:本程序版權歸作者所有,允許以學習目的的傳播,
 * 但必須保留本作者署名及源程序的完整!
 
*/


package  com.qnjian.notpad;

import  javax.swing. * ;
import  java.io. * ;
import  java.awt. * ;
import  java.awt.event. * ;

public   class  NotePad  extends  JFrame  implements  ActionListener,ItemListener
{
    
boolean  haveCreated = false ;
    File file
= null ;
    String strtext
= "" ;
    
int  findIndex = 0 ;
    String findStr;
    
      JMenuBar menubar   
=   new  JMenuBar();
    JMenu meFile       
=   new  JMenu( " 文件 " );
    JMenu meEdit       
=   new  JMenu( " 編輯 " );
    JMenu meStyle      
=   new  JMenu( " 風格 " );
    JMenu meHelp       
=   new  JMenu( " 幫助 " );
        
    JMenuItem miCreate 
=   new  JMenuItem( " 新建 " );    
    JMenuItem miOpen   
=   new  JMenuItem( " 打開 " );    
    JMenuItem miSave   
=   new  JMenuItem( " 保存 " );    
    JMenuItem miSaveAs 
=   new  JMenuItem( " 另存爲 " );    
    JMenuItem miExit   
=   new  JMenuItem( " 退出 " );    
    
    JMenuItem miUndo   
=   new  JMenuItem( " 撤消 " );    
    JMenuItem miCut    
=   new  JMenuItem( " 剪切 " );    
    JMenuItem miCopy   
=   new  JMenuItem( " 複製 " );    
    JMenuItem miPaste  
=   new  JMenuItem( " 粘貼 " );    
    JMenuItem miDelete 
=   new  JMenuItem( " 刪除 " );    
    JMenuItem miFind   
=   new  JMenuItem( " 查找 " );    
    JMenuItem miNext   
=   new  JMenuItem( " 查找下一個 " );    
    JMenuItem miReplace
=   new  JMenuItem( " 替換 " );
    
    
// 右鍵彈出菜單項
    JMenuItem pmUndo    =   new  JMenuItem( " 撤消 " );
    JMenuItem pmCut    
=   new  JMenuItem( " 剪切 " );        
    JMenuItem pmCopy   
=   new  JMenuItem( " 複製 " );        
    JMenuItem pmPaste  
=   new  JMenuItem( " 粘貼 " );        
    JMenuItem pmDelete 
=   new  JMenuItem( " 刪除 " );
    
    JCheckBoxMenuItem miNewLine
=   new  JCheckBoxMenuItem( " 自動換行 " );
    JMenu smLookFeel   
=   new  JMenu( " 觀感 " );
    JMenuItem metal    
=   new  JMenuItem( " Metal " );
    JMenuItem motif    
=   new  JMenuItem( " Motif " );
    JMenuItem windows  
=   new  JMenuItem( " Windows " );
        
    JMenuItem miAbout  
=   new  JMenuItem( " 關於 " );
    
    JPopupMenu popupMenu;    
    JTextArea text     
=   new  JTextArea();

        
    
public  NotePad()
    
{
        
super ( " 我的記事本 " );
        
// 爲便於區分事件源,設定名字
        miCreate.setActionCommand( " create " );
        miOpen.setActionCommand(
" open " );
        miSave.setActionCommand(
" save " );
        miSaveAs.setActionCommand(
" saveAs " );
        miExit.setActionCommand(
" exit " );        
        
        miUndo.setActionCommand(
" undo " );
        miCut.setActionCommand(
" cut " );
        miCopy.setActionCommand(
" copy " );
        miPaste.setActionCommand(
" paste " );
        miDelete.setActionCommand(
" delete " );
        miFind.setActionCommand(
" find " );
        miNext.setActionCommand(
" next " );
        miReplace.setActionCommand(
" replace " );
        
        miNewLine.setActionCommand(
" newLine " );    
        miAbout.setActionCommand(
" about " );
        
        pmUndo.setActionCommand(
" undo " );
        pmCut.setActionCommand(
" cut " );
        pmCopy.setActionCommand(
" copy " );
        pmPaste.setActionCommand(
" paste " );
        pmDelete.setActionCommand(
" delete " );
        
        
this .setSize( 500 , 500 );
        
this .setLocation( 300 , 150 );
        
this .setJMenuBar(menubar);
        
        meFile.setFont(
new  Font( " 宋體 " ,Font.BOLD, 15 ));
        meEdit.setFont(
new  Font( " 宋體 " ,Font.BOLD, 15 ));
        meStyle.setFont(
new  Font( " 宋體 " ,Font.BOLD, 15 ));
        meHelp.setFont(
new  Font( " 宋體 " ,Font.BOLD, 15 ));
        
        menubar.add(meFile);
        menubar.add(meEdit);
        menubar.add(meStyle);
        menubar.add(meHelp);
        
        meFile.add(miCreate);
        meFile.add(miOpen);
        meFile.add(miSave);
        meFile.add(miSaveAs);
        meFile.addSeparator();
        meFile.add(miExit);    
        
        meEdit.add(miUndo);
        meEdit.addSeparator();        
        meEdit.add(miCut);
        meEdit.add(miCopy);
        meEdit.add(miPaste);
        meEdit.add(miDelete);
        meEdit.addSeparator();
        meEdit.add(miFind);
        meEdit.add(miNext);
        meEdit.addSeparator();
        meEdit.add(miReplace);
        
        meStyle.add(miNewLine);
        meStyle.add(smLookFeel);
        smLookFeel.add(metal);
        smLookFeel.add(motif);
        smLookFeel.add(windows);
        
        meHelp.add(miAbout);
        
// 添加到右鍵彈出菜單
        popupMenu = new  JPopupMenu();
        popupMenu.add(pmUndo);
        popupMenu.addSeparator();
        popupMenu.add(pmCut);
        popupMenu.add(pmCopy);
        popupMenu.add(pmPaste);
        popupMenu.add(pmDelete);
        
// 添加按鈕事件監聽
        meHelp.addActionListener( this );
        miCreate.addActionListener(
this );
        miOpen.addActionListener(
this );
        miSave.addActionListener(
this );
        miSaveAs.addActionListener(
this );
        miExit.addActionListener(
this );
        
        miUndo.addActionListener(
this );
        miCut.addActionListener(
this );
        miCopy.addActionListener(
this );
        miPaste.addActionListener(
this );
        miDelete.addActionListener(
this );
        miFind.addActionListener(
this );
        miNext.addActionListener(
this );
        miReplace.addActionListener(
this );
        
        miNewLine.addItemListener(
this );                
        miAbout.addActionListener(
this );
        metal.addActionListener(
this );
        motif.addActionListener(
this );
        windows.addActionListener(
this );
        
        
// 添加右鍵按鈕事件監聽器
        pmUndo.addActionListener( this );
        pmCut.addActionListener(
this );
        pmCopy.addActionListener(
this );
        pmPaste.addActionListener(
this );
        pmDelete.addActionListener(
this );
        
        
// 文本區內容沒有選中時某些按鈕不可用
        miCut.setEnabled( false );
        miCopy.setEnabled(
false );
        miDelete.setEnabled(
false );
        
        pmCut.setEnabled(
false );
        pmCopy.setEnabled(
false );
        pmDelete.setEnabled(
false );
            
        JScrollPane scrollPane 
= new  JScrollPane(text);    
        getContentPane().add(scrollPane,
" Center " );
        text.setFont(
new  Font( " Fixedsys " ,Font.TRUETYPE_FONT, 15 ));                
        setVisible(
true );
        
        
// 添加鍵盤輸入監聽器
        text.addFocusListener( new  MyFocusAdapter());
        
// 添加鼠標監聽器,用於激活右鍵彈出菜單
        text.addMouseListener( new  MouseAdapter()
            
{
                
public   void  mouseReleased(MouseEvent e)
                
{
                    
if (e.isPopupTrigger())
                    
{
                        popupMenu.show(e.getComponent(),e.getX(),e.getY());
                    }

                }

            }
);
        
// 添加窗口關閉監聽器    
        addWindowListener( new  WindowAdapter()
            
{
                
public   void  windowClosing(WindowEvent e)
                
{ // 詢問是否保存時選擇撤消
                     int  i;
                    
if ( (i = askForSave()) == 3 )
                    
{
                        
return ;    
                    }

                    System.exit(
0 );                    
                }

            }
);        
            
    
    }

////////////////////////// Methods //////////////////////////////////// /
    
// 打開
     public   void  open()
    
{        
        JFileChooser jc 
= new  JFileChooser();
        jc.showOpenDialog(
this );
        File f 
=  jc.getSelectedFile();
        
if (f == null ) // 沒有選擇文件則退出
         {
            
return ;
        }

        file
= f; // file 是File類的對象,爲本類屬性,在保存當前內容時用
         this .setTitle(f.getName() + " --記事本 " );        
        FileReader fr
= null ;
        
int  len = ( int )f.length();
        
char [] ch = new   char [len];
        
int  num = 0 ;
        
try
        
{
            fr
=   new  FileReader(f);
            
while (fr.ready())
            
{
                num
+= fr.read(ch,num,len - num);
            }

            
// 保存在屬性strtext中,爲了便於撤消恢復及監視內容是否改變            
            strtext = new  String(ch, 0 ,num);
            haveCreated
= false ;
            text.setText(strtext);
        }

        
catch (Exception e)
        
{
            JOptionPane.showMessageDialog(
this , " 出錯:  " + e.getMessage());
        }

        
finally
        
{
            
try
            
{
                fr.close();                
            }

            
catch (IOException e)
            
{
                JOptionPane.showMessageDialog(
this , " 出錯:  " + e.getMessage());
            }

        }
        
    }

    
// 保存
     public   void  save(File f)
    
{
        String saveStr
= text.getText();
        FileWriter fw
= null ;
        
try
        
{
            fw
=   new  FileWriter(f);
            fw.write(saveStr);
            fw.flush();
        }

        
catch (Exception e)
        
{
            JOptionPane.showMessageDialog(
this , " 出錯:  " + e.getMessage());
        }

        
finally
        
{
            
try
            
{
                fw.close();                
            }

            
catch (IOException e)
            
{
                JOptionPane.showMessageDialog(
this , " 出錯:  " + e.getMessage());
            }

        }

        haveCreated
= false ;
        JOptionPane.showMessageDialog(
this , " 文件保存成功! " );        
    }

    
// 另存爲
     public   void  saveAs()
    
{
        JFileChooser fs 
= new  JFileChooser();
        fs.showSaveDialog(
this );
        File f 
=  fs.getSelectedFile();
        
if (f != null )
        
{
            save(f);
            
this .setTitle(f.getName() + " --記事本 " );
            file
= f;
        }

        
    }

    
/** 如果顯示的文件內容與原來有改變,詢問是否保存
     *
@return  int 0: no operation  1:yes  2:no   3:cancel -1:error return
     
*/

    
public   int  askForSave()
    
{    
        
if (haveCreated && text.getText() == "" )
        
{
            
return   0 ;
        }

        
        
if (text.getText().equals(strtext) == false )
        
{
            String fn;
            
if (file != null )
            
{
                fn
= "" + file.getName();
            }

            
else
            
{
                fn
= " 未命名 " ;
            }

            
int  i = JOptionPane.showConfirmDialog( this , " 文件 " + fn + " 的文字已經改變。 " +
            
" /n要保存文件嗎? " , " 記事本 " ,JOptionPane.YES_NO_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);
            
            
if (i == JOptionPane.YES_OPTION)
            
{
                
if (file == null )
                
{
                    saveAs();
                }

                
else
                
{
                    save(file);
                }

                
return   1 ;
                
            }

            
if (i == JOptionPane.NO_OPTION)
            
{
                
return   2 ;
            }

            
if (i == JOptionPane.CANCEL_OPTION)
            
{
                
return   3 ;
            }

        }

        
return   - 1 ;
    }

    
// 查找
     public   void  find(String str,  int  cur)
    
{
        
int  i = text.getText().indexOf(str,cur);
        
if (i >= 0 )
        
{
            
this .text.setSelectionStart(i); // 使找到的字符串反白選中
             this .text.setSelectionEnd(i + str.length());
            findIndex
=++ i; // 用於查找下一個
        }

        
else
        
{
            JOptionPane.showMessageDialog(
this , " 查找完畢! " " 記事本 " ,
                JOptionPane.OK_OPTION 
+  JOptionPane.INFORMATION_MESSAGE);    
        }
    
    }

    
// 替換全部
     public   void  replaceAll(String fromStr,String toStr, int  cur, int  end)
    
{
        
if (cur > end)
        
{        
            
return ;
        }

        
else
        
{    
            
int  i = text.getText().indexOf(fromStr,cur);        
            
if (i >= 0 )
            
{
                text.setSelectionStart(i);
// 使找到的字符串反白選中
                text.setSelectionEnd(i + fromStr.length());
                text.replaceSelection(toStr);
// 替換
                cur =++ i;
            }

            
else
            
{
                JOptionPane.showMessageDialog(
this , " 替換完畢! " " 記事本 " ,
                JOptionPane.OK_OPTION 
+  JOptionPane.INFORMATION_MESSAGE);
                
return ;    
            }

            replaceAll(fromStr,toStr,cur,end); 
// 遞歸查找與替換
        }

    }

    
// 切換觀感    
     public   void  changeLookFeel(Component c, String plafName)
    
{
        
try
        
{
            UIManager.setLookAndFeel(plafName);    
            SwingUtilities.updateComponentTreeUI(c);
        }

        
catch (Exception e)
        
{
            JOptionPane.showMessageDialog(
this , " 觀感加載失敗! " , " 記事本 " ,
                JOptionPane.YES_OPTION
+ JOptionPane.INFORMATION_MESSAGE);
        }

    }

//////////////////////// 實現監聽類的方法(本類實現了相應接口) ////////////////////////////////// /


    
public   void  itemStateChanged(ItemEvent e)
    
{
        
if (e.getStateChange() == e.SELECTED)
        
{
            text.setLineWrap(
true );
        }

        
else
        
{
            text.setLineWrap(
false );
        }

    }

    
    
public   void  actionPerformed(ActionEvent e)
    
{
        String com
= e.getActionCommand();
        
if (com.equals( " create " ))
        
{     // 當前顯示的文件內容如有改變,詢問是否保存
            
// 返回:詢問是否保存時選擇否        
            
// 返回3:詢問是否保存時選擇撤消
             int  i;
            
if ((i = askForSave()) == 3 )
            
{
                
return ;
            }
        
            text.setText(
"" );
            file
= null ;
            
this .setTitle( " 未命名--記事本 " );
            strtext
= "" ;                    
            haveCreated
= true ;        
        }

        
if (com.equals( " open " ))
        
{ // 詢問是否保存時選擇撤消
             int  i;
            
if ((i = askForSave()) == 3 )
            
{
                
return ;
            }
        
            open();
            strtext
= text.getText();
        }

        
if (com.equals( " saveAs " ))
        
{
            saveAs();
            strtext
= text.getText();
        }

        
if (com.equals( " save " ))
        
{
            
if (haveCreated || file == null )
            
{
                saveAs();
            }

            
else
            
{
                save(file);
            }

            strtext
= text.getText();            
        }

        
if (com.equals( " undo " ))
        
{
            text.setText(strtext);
        }

        
if (com.equals( " cut " ))
        
{
            
// 先保存文本區內容,供撤消後恢復使用
            strtext = text.getText();
            text.cut();    
        }

        
if (com.equals( " copy " ))
        
{
            text.copy();
        }

        
if (com.equals( " paste " ))
        
{
            strtext
= text.getText();
            text.paste();
        }

        
if (com.equals( " delete " ))
        
{
            strtext
= text.getText();
            text.replaceSelection(
"" );
        }

        
if (com.equals( " find " ))
        
{    
            findIndex
= 0 ;            
            FindDialog fd 
=   new  FindDialog( this );                    
        }

        
if (com.equals( " next " ))
        
{
            
// 沒有選中內容則從頭開始查找
            String str  =  text.getSelectedText();
            
if (str == "" || str == null )
            
{
                findIndex
= 0 ;
            }

            
if (str == null )
            
{
                FindDialog fd 
=   new  FindDialog( this );
            }

            
else
            
{
                find(str,text.getSelectionStart()
+ 1 );
            }
    
        }

        
if (com.equals( " replace " ))
        
{
            ReplaceDialog rd 
=   new  ReplaceDialog( this );
        }
        
        
if (com.equals( " about " ))
        
{
            JOptionPane.showMessageDialog(
this , " 我的記事本  V1.0/n作者:ZhiJian Zhang /n2005/5/25 Copyright " ,
                
" 關於我的記事本 " ,JOptionPane.OK_OPTION + JOptionPane.INFORMATION_MESSAGE);
        }
    
        
// 觀感選擇
         if (e.getActionCommand().equals( " Metal " ))
        
{
            String metal_str
= " javax.swing.plaf.metal.MetalLookAndFeel " ;
            changeLookFeel(
this ,metal_str);
        }

        
if (e.getActionCommand().equals( " Motif " ))
        
{
            String motif_str
= " com.sun.java.swing.plaf.motif.MotifLookAndFeel " ;
            changeLookFeel(
this ,motif_str);
        }

        
if (e.getActionCommand().equals( " Windows " ))
        
{
            
// String windows_str="com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel"
            String windows_str = " com.sun.java.swing.plaf.windows.WindowsLookAndFeel " ;
            changeLookFeel(
this ,windows_str);    
        }
                    
        
if (e.getActionCommand().equals( " exit " ))
        
{
            
int  i;
            
if ( (i = askForSave()) == 3 )
            
{
                
return ;
            }

            System.exit(
0 );
        }
        
    }

////////////////////////////// /內部監聽類 ////////////////////////////
// 監聽文本區變動
     class  MyFocusAdapter  extends  FocusAdapter
    
{ // 如果文本區沒有選中的內容,則相應菜單項不可用
         public   void  focusLost(FocusEvent e)
        
{
            String str 
=  text.getSelectedText();
            
if (str != "" && str != null )
            
{
                
// 菜單項
                miCut.setEnabled( true );
                miCopy.setEnabled(
true );
                miDelete.setEnabled(
true );
                
// 右鍵菜單項
                pmCut.setEnabled( true );
                pmCopy.setEnabled(
true );
                pmDelete.setEnabled(
true );
            }

            
else
            
{
                miCut.setEnabled(
false );
                miCopy.setEnabled(
false );
                miDelete.setEnabled(
false );
            
                pmCut.setEnabled(
false );
                pmCopy.setEnabled(
false );
                pmDelete.setEnabled(
false );    
            }
            
        }

    }
    
    
public   static   void  main(String[] args)
    
{
        NotePad nt 
=   new  NotePad();
        
    }

    
}
// End of main class

////////////////////////// 外部類 ////////////////////////////////
////////////////////
/查找對對話框類 ////////////////////////////
class  FindDialog  extends  JDialog  implements  ActionListener
{
    
// 引入np屬性是爲了通過構造器的參數傳入NotePad的對象,
    
// 從而方便訪問其內部屬性與方法
    NotePad np;
    JTextField tex;
    JLabel label;
    JButton find;
    JButton exit;
    
public  FindDialog(NotePad owener)
    
{
        
super (owener, " 查找 " , false );
         
this .np  =  owener;
         label   
=   new  JLabel( "   查找內容 " );
         tex    
=   new  JTextField( 5 );            
         find    
=   new  JButton( " 查找下一個 " );
         exit    
=   new  JButton( " 取消 " );
         Container contentPane 
=  getContentPane();
         contentPane.setLayout(
new  GridLayout( 2 , 2 ));
         contentPane.add(label);
         contentPane.add(tex);
         contentPane.add(find);
         contentPane.add(exit);
         
this .setSize( 210 , 80 );
         
this .setLocation( 450 , 350 );         
         
this .setResizable( false );
         
         find.addActionListener(
this );
         exit.addActionListener(
this );
         tex.addKeyListener(
new  MyKeyAdapter());
         find.setEnabled(
false );         
         
this .setVisible( true );
         
    }

    
public   void  actionPerformed(ActionEvent e)
    
{
        String str
= np.text.getSelectedText();
        
if (str == "" || str == null )
        
{
            np.findIndex
= 0 ;
        }

        
if (e.getSource() == find)
        
{            
            np.find(tex.getText(),np.findIndex);
        }

        
else   if (e.getSource() == exit)
        
{
            
this .dispose();
        }

    }

    
class  MyKeyAdapter  extends  KeyAdapter
    
{        
        
public   void  keyReleased(KeyEvent e)
        
{
            
if (np.text.getSelectedText() != "" )
            
{
                find.setEnabled(
true );
            }

        }

    }

}

////////////////////// 查找替換對話框 //////////////////////// /
class  ReplaceDialog  extends  JDialog  implements  ActionListener
{
    NotePad np;
    JTextField ft;
    JTextField rt;
    JLabel b1;
    JLabel b2;
    JButton find;
    JButton rp;
    JButton rpa;
    JButton exit;
    
public  ReplaceDialog(NotePad owener)
    
{
        
super (owener, " 替換 " , false );
         
this .np  =  owener;
         b1 
=   new  JLabel( " 查找內容 " );
         b2 
=   new  JLabel( " 替  換  爲  " );
         ft
= new  JTextField( 8 );
         
// 初始狀態將已查找到的字符串放在此文本框中
         ft.setText(owener.text.getSelectedText());
         rt
= new  JTextField( 8 );
                     
         find 
=   new  JButton( " 查找下一個 " );
         rp   
= new  JButton( " 替     換 " );
         rpa  
= new  JButton( " 全部替換 " );
         exit    
=   new  JButton( " 取       消 " );
         Container cp 
=  getContentPane();
         JPanel p1
=   new  JPanel();
         JPanel p2
=   new  JPanel();
         JPanel p3
=   new  JPanel();
         
         p1.setLayout(
new  GridLayout( 2 , 3 , 5 , 2 ));
         p1.add(b1);
         p1.add(ft);
         p1.add(find);
         p1.add(b2);
         p1.add(rt);
         p1.add(rp);         
         cp.add(p1,BorderLayout.NORTH);
         
         p2.setLayout(
new  FlowLayout(FlowLayout.RIGHT));
         p2.add(rpa);
         p2.add(exit);
         cp.add(p2,BorderLayout.SOUTH);     
         
         
this .setSize( 350 , 120 );
         
this .setLocation( 400 , 350 );         
         
this .setResizable( false );
         
         find.addActionListener(
this );
         rp.addActionListener(
this );
         rpa.addActionListener(
this );
         exit.addActionListener(
this );
         
         ft.addKeyListener(
new  MyKeyAdapter());
         rt.addKeyListener(
new  MyKeyAdapter());
         find.setEnabled(
false );
         rp.setEnabled(
false );
         rpa.setEnabled(
false );
                  
         
this .setVisible( true );
    }

    
    
public   void  actionPerformed(ActionEvent e)
    
{        
        
if (e.getSource() == find)
        
{    
            
/* String str=np.text.getSelectedText();
            if(str==""||str==null)
            {
                np.findIndex=0;
            }
*/

            
if (ft.getText().equals(np.findStr) == false )
            
{
                np.findIndex
= 0 ;
            }
    
            np.findStr
= ft.getText();    
            np.find(np.findStr,np.findIndex);                
        }

        
if (e.getSource() == rp)
        
{
            String str
= np.text.getSelectedText();
            
if (str != "" && str != null )
            
{
                np.text.replaceSelection(rt.getText());
            }
            
        }

        
if (e.getSource() == rpa)
        
{
            
int  n = np.text.getText().length();
            np.replaceAll(ft.getText(),rt.getText(),
0 ,n);
        }

        
else   if (e.getSource() == exit)
        
{
            
this .dispose();
        }

    }

    
class  MyKeyAdapter  extends  KeyAdapter
    
{ // 如果查找或替換框內沒有內容則相應按鈕不可用        
         public   void  keyReleased(KeyEvent e)
        
{
            
if (ft.getText() != "" )
            
{
                find.setEnabled(
true );
            }

            
if (rt.getText() != "" )
            
{
                rp.setEnabled(
true );
                rpa.setEnabled(
true );
            }

        }

    }

}



      我將上面源代碼編譯後,打成可執行的Jar包,運行效果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章