2014-12-01 23 views
5

Trên ứng dụng tôi hiện đang làm việc, cần phải chọn một ngày hoặc một khoảng thời gian từ cùng một JavaFX 8 DatePicker.Chọn một khoảng thời gian hoặc ngày bằng cách sử dụng ONE JavaFX 8 DatePicker

Cách ưa thích để làm điều này sẽ như sau:

  1. Lựa chọn một ngày duy nhất - tương tự như hành vi mặc định của DatePicker.

  2. Chọn khoảng thời gian - chọn ngày bắt đầu/kết thúc bằng cách giữ nút chuột và kéo đến ngày kết thúc/ngày bắt đầu bạn muốn. Khi nút chuột được nhả, bạn đã xác định thời gian của bạn. Thực tế là bạn không thể chọn ngày khác với những ngày được hiển thị có thể chấp nhận được.

  3. Editing nên làm việc cho cả ngày đơn (ví dụ 2014/12/24) và thời gian (ví dụ: 2014/12/24 - 27.12.2014)

Một vẽ có thể có của giai đoạn lựa chọn (trừ nội dung của soạn thảo văn bản) ở trên sẽ giống như thế này:

Rendering of selected period

đâu màu cam cho ngày hiện tại, màu xanh chỉ ra khoảng thời gian được chọn. Hình ảnh là từ một nguyên mẫu tôi đã tạo, nhưng khi khoảng thời gian được chọn bằng cách sử dụng 2 DatePickers thay vì một.

tôi đã có một cái nhìn tại các sourcecode cho

com.sun.javafx.scene.control.skin.DatePickerContent 

trong đó có một

protected List<DateCell> dayCells = new ArrayList<DateCell>(); 

để tìm ra một cách để phát hiện khi chuột chọn một ngày kết thúc khi con chuột được phát hành (hoặc có thể phát hiện một kéo).

Tuy nhiên tôi không chắc chắn về cách thực hiện. Bất kỳ đề xuất?

Tôi đang gắn mã mẫu đơn giản mà tôi đã thực hiện cho đến thời điểm này (sử dụng 2 chứ không phải là 1 datepicker mong muốn).

Prototype so far

import java.time.LocalDate; 

import javafx.beans.property.SimpleObjectProperty; 

public interface PeriodController { 

    /** 
    * @return Today. 
    */ 
    LocalDate currentDate(); 

    /** 
    * @return Selected from date. 
    */ 
    SimpleObjectProperty<LocalDate> fromDateProperty(); 

    /** 
    * @return Selected to date. 
    */ 
    SimpleObjectProperty<LocalDate> toDateProperty(); 
} 


import java.time.LocalDate; 
import java.time.format.DateTimeFormatter; 

import javafx.util.StringConverter; 

public class DateConverter extends StringConverter<LocalDate> { 

    private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); // TODO i18n 

    @Override 
    public String toString(LocalDate date) { 
     if (date != null) { 
      return dateFormatter.format(date); 
     } else { 
      return ""; 
     } 
    } 

    @Override 
    public LocalDate fromString(String string) { 
     if (string != null && !string.isEmpty()) { 
      return LocalDate.parse(string, dateFormatter); 
     } else { 
      return null; 
     } 
    } 


} 







import static java.lang.System.out; 

import java.time.LocalDate; 
import java.util.Locale; 

import javafx.application.Application; 
import javafx.beans.property.SimpleObjectProperty; 
import javafx.beans.value.ChangeListener; 
import javafx.geometry.HPos; 
import javafx.scene.Scene; 
import javafx.scene.control.Label; 
import javafx.scene.layout.GridPane; 
import javafx.scene.layout.VBox; 
import javafx.stage.Stage; 

public class PeriodMain extends Application { 

    private Stage stage; 

    public static void main(String[] args) { 
     Locale.setDefault(new Locale("no", "NO")); 
     launch(args); 
    } 

    @Override 
    public void start(Stage stage) { 
     this.stage = stage; 
     stage.setTitle("Period prototype "); 
     initUI(); 
     stage.getScene().getStylesheets().add(getClass().getResource("/period-picker.css").toExternalForm()); 
     stage.show(); 
    } 

    private void initUI() { 
     VBox vbox = new VBox(20); 
     vbox.setStyle("-fx-padding: 10;"); 
     Scene scene = new Scene(vbox, 400, 200); 


     stage.setScene(scene); 
     final PeriodPickerPrototype periodPickerPrototype = new PeriodPickerPrototype(new PeriodController() { 

      SimpleObjectProperty<LocalDate> fromDate = new SimpleObjectProperty<>(); 
      SimpleObjectProperty<LocalDate> toDate = new SimpleObjectProperty<>(); 

      { 
       final ChangeListener<LocalDate> dateListener = (observable, oldValue, newValue) -> { 
        if (fromDate.getValue() != null && toDate.getValue() != null) { 
         out.println("Selected period " + fromDate.getValue() + " - " + toDate.getValue()); 
        } 
       }; 
       fromDate.addListener(dateListener); 
       toDate.addListener(dateListener); 

      } 


      @Override public LocalDate currentDate() { 
       return LocalDate.now(); 
      } 

      @Override public SimpleObjectProperty<LocalDate> fromDateProperty() { 
       return fromDate; 
      } 

      @Override public SimpleObjectProperty<LocalDate> toDateProperty() { 
       return toDate; 
      } 


     }); 

     GridPane gridPane = new GridPane(); 
     gridPane.setHgap(10); 
     gridPane.setVgap(10); 
     Label checkInlabel = new Label("Check-In Date:"); 
     GridPane.setHalignment(checkInlabel, HPos.LEFT); 
     gridPane.add(periodPickerPrototype, 0, 1); 
     vbox.getChildren().add(gridPane); 
    } 
} 







import java.time.LocalDate; 

import javafx.beans.value.ChangeListener; 
import javafx.event.ActionEvent; 
import javafx.event.EventHandler; 
import javafx.scene.control.DateCell; 
import javafx.scene.control.DatePicker; 
import javafx.scene.control.Label; 
import javafx.scene.control.Tooltip; 
import javafx.scene.layout.GridPane; 
import javafx.util.Callback; 
import javafx.util.StringConverter; 


/** 
* Selecting a single date or a period - only a prototype. 
* As long as you have made an active choice on the {@code toDate}, the {@code fromDate} and {@code toDate} will have the same date. 
*/ 
public class PeriodPickerPrototype extends GridPane { 

    private static final String CSS_CALENDAR_BEFORE = "calendar-before"; 
    private static final String CSS_CALENDAR_BETWEEN = "calendar-between"; 
    private static final String CSS_CALENDAR_TODAY = "calendar-today"; 
    private static final boolean DISPLAY_WEEK_NUMBER = true; 

    private Label fromLabel; 
    private Label toLabel; 

    private DatePicker fromDate; 
    private DatePicker toDate; 
    private StringConverter<LocalDate> converter; 
    private PeriodController controller; 
    private ChangeListener<LocalDate> fromDateListener; 
    private ChangeListener<LocalDate> toDateListener; 
    private Callback<DatePicker, DateCell> toDateCellFactory; 
    private Callback<DatePicker, DateCell> fromDateCellFactory; 
    private Tooltip todayTooltip; 
    private boolean toDateIsActivlyChosenbyUser; 

    public PeriodPickerPrototype(final PeriodController periodController) 

    { 
     this.controller = periodController; 
     createComponents(); 
     makeLayout(); 
     createHandlers(); 
     bindAndRegisterHandlers(); 
     i18n(); 
     initComponent(); 
    } 

    public void createComponents() { 
     fromLabel = new Label(); 
     toLabel = new Label(); 
     fromDate = new DatePicker(); 
     toDate = new DatePicker(); 
     todayTooltip = new Tooltip(); 
    } 

    public void createHandlers() { 
     fromDate.setOnAction(event -> { 
      if ((!toDateIsActivlyChosenbyUser) || fromDate.getValue().isAfter(toDate.getValue())) { 
       setDateWithoutFiringEvent(fromDate.getValue(), toDate); 
       toDateIsActivlyChosenbyUser = false; 
      } 

     }); 

     toDate.setOnAction(event -> toDateIsActivlyChosenbyUser = true); 

     fromDateCellFactory = new Callback<DatePicker, DateCell>() { 
      @Override public DateCell call(final DatePicker datePicker) { 
       return new DateCell() { 
        @Override 
        public void updateItem(LocalDate item, boolean empty) { 
         super.updateItem(item, empty); 
         getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN); 

         if ((item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) && item.isAfter(fromDate.getValue())) { 
          getStyleClass().add(CSS_CALENDAR_BETWEEN); 
         } 

         if (item.isEqual(controller.currentDate())) { 
          getStyleClass().add(CSS_CALENDAR_TODAY); 
          setTooltip(todayTooltip); 
         } else { 
          setTooltip(null); 
         } 
        } 
       }; 
      } 
     }; 

     toDateCellFactory = 
       new Callback<DatePicker, DateCell>() { 
        @Override 
        public DateCell call(final DatePicker datePicker) { 
         return new DateCell() { 
          @Override 
          public void updateItem(LocalDate item, boolean empty) { 
           super.updateItem(item, empty); 
           setDisable(item.isBefore(fromDate.getValue())); 
           getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN); 


           if (item.isBefore(fromDate.getValue())) { 
            getStyleClass().add(CSS_CALENDAR_BEFORE); 
           } else if (item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) { 
            getStyleClass().add(CSS_CALENDAR_BETWEEN); 
           } 
           if (item.isEqual(controller.currentDate())) { 
            getStyleClass().add(CSS_CALENDAR_TODAY); 
            setTooltip(todayTooltip); 
           } else { 
            setTooltip(null); 
           } 
          } 
         }; 
        } 
       }; 
     converter = new DateConverter(); 
     fromDateListener = (observableValue, oldValue, newValue) -> { 
      if (newValue == null) { 
       // Restting old value and cancel.. 
       setDateWithoutFiringEvent(oldValue, fromDate); 
       return; 
      } 
      controller.fromDateProperty().set(newValue); 
     }; 
     toDateListener = (observableValue, oldValue, newValue) -> { 
      if (newValue == null) { 
       // Restting old value and cancel.. 
       setDateWithoutFiringEvent(oldValue, toDate); 
       return; 
      } 
      controller.toDateProperty().set(newValue); 
     }; 

    } 

    /** 
    * Changes the date on {@code datePicker} without fire {@code onAction} event. 
    */ 
    private void setDateWithoutFiringEvent(LocalDate newDate, DatePicker datePicker) { 
     final EventHandler<ActionEvent> onAction = datePicker.getOnAction(); 
     datePicker.setOnAction(null); 
     datePicker.setValue(newDate); 
     datePicker.setOnAction(onAction); 
    } 

    public void bindAndRegisterHandlers() { 
     toDate.setDayCellFactory(toDateCellFactory); 
     fromDate.setDayCellFactory(fromDateCellFactory); 
     fromDate.valueProperty().addListener(fromDateListener); 
     fromDate.setConverter(converter); 
     toDate.valueProperty().addListener(toDateListener); 
     toDate.setConverter(converter); 

    } 

    public void makeLayout() { 
     setHgap(6); 
     add(fromLabel, 0, 0); 
     add(fromDate, 1, 0); 
     add(toLabel, 2, 0); 
     add(toDate, 3, 0); 

     fromDate.setPrefWidth(120); 
     toDate.setPrefWidth(120); 
     fromLabel.setId("calendar-label"); 
     toLabel.setId("calendar-label"); 
    } 

    public void i18n() { 
     // i18n code replaced with 
     fromDate.setPromptText("dd.mm.yyyy"); 
     toDate.setPromptText("dd.mm.yyyy"); 
     fromLabel.setText("From"); 
     toLabel.setText("To"); 
     todayTooltip.setText("Today"); 
    } 

    public void initComponent() { 
     fromDate.setTooltip(null); // Ønsker ikke tooltip 
     setDateWithoutFiringEvent(controller.currentDate(), fromDate); 
     fromDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER); 

     toDate.setTooltip(null); // Ønsker ikke tooltip 
     setDateWithoutFiringEvent(controller.currentDate(), toDate); 
     toDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER); 
    } 


} 

/** period-picker.css goes udner resources (using maven) **/ 

.date-picker { 
    /* -fx-font-size: 11pt;*/ 
} 

.calendar-before { 
} 

.calendar-between { 
    -fx-background-color: #bce9ff; 
} 

.calendar-between:hover { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-between:focused { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-today { 
    -fx-background-color: rgb(255, 218, 111); 
} 

.calendar-today:hover { 
    -fx-background-color: rgb(0, 150, 201); 
} 

.calendar-today:focused { 
    -fx-background-color: rgb(0, 150, 201); 
} 

#calendar-label { 
    -fx-font-style: italic; 
    -fx-fill: rgb(75, 75, 75); 
    -fx-font-size: 11; 
} 

Trả lời

5

Tôi nghĩ rằng bạn đã có trong đi đúng hướng ... DateCell và kéo có thể làm việc, kể từ khi popup không đóng nếu một sự kiện kéo được phát hiện hoặc khi nó kết thúc. Điều đó mang đến cho bạn cơ hội theo dõi các ô được người dùng chọn.

Đây là một hack nhanh chóng, nhưng nó có thể giúp bạn với việc lựa chọn phạm vi.

Đầu tiên sẽ lấy nội dung và danh sách tất cả các ô trong tháng được hiển thị, thêm người nghe để kéo sự kiện, đánh dấu ô đầu tiên nơi kéo bắt đầu và chọn tất cả ô trong ô đầu tiên này và ô bên dưới vị trí chuột thực tế, bỏ chọn phần còn lại.

Sau khi sự kiện kéo hoàn tất, phạm vi đã chọn được hiển thị trên bảng điều khiển. Và bạn có thể bắt đầu lại từ đầu cho đến khi cửa sổ bật lên bị đóng.

private DateCell iniCell=null; 
private DateCell endCell=null; 

@Override 
public void start(Stage primaryStage) { 
    DatePicker datePicker=new DatePicker(); 
    datePicker.setValue(LocalDate.now()); 

    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250); 

    primaryStage.setScene(scene); 
    primaryStage.show(); 

    datePicker.showingProperty().addListener((obs,b,b1)->{ 
     if(b1){ 
      DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent(); 

      List<DateCell> cells = content.lookupAll(".day-cell").stream() 
        .filter(ce->!ce.getStyleClass().contains("next-month")) 
        .map(n->(DateCell)n) 
        .collect(Collectors.toList()); 

      content.setOnMouseDragged(e->{ 
       Node n=e.getPickResult().getIntersectedNode(); 
       DateCell c=null; 
       if(n instanceof DateCell){ 
        c=(DateCell)n; 
       } else if(n instanceof Text){ 
        c=(DateCell)(n.getParent()); 
       } 
       if(c!=null && c.getStyleClass().contains("day-cell") && 
         !c.getStyleClass().contains("next-month")){ 
        if(iniCell==null){ 
         iniCell=c; 
        } 
        endCell=c; 
       } 
       if(iniCell!=null && endCell!=null){ 
        int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
      }); 
      content.setOnMouseReleased(e->{ 
       if(iniCell!=null && endCell!=null){ 
        System.out.println("Selection from "+iniCell.getText()+" to "+endCell.getText()); 
       } 
       endCell=null; 
       iniCell=null;      
      }); 
     } 
    }); 
} 

Và đây là cách nó trông giống như:

Range selection on DatePicker

Còn bây giờ điều này không cập nhật các textfield, vì điều này liên quan đến việc sử dụng một định dạng tùy chỉnh.

EDIT

Tôi đã thêm một bộ chuyển đổi chuỗi tùy chỉnh để hiển thị phạm vi trên TextField, sau khi lựa chọn được thực hiện, và cũng để chọn dải nếu một giá trị được nhập vào.

Đây không phải là bằng chứng đạn, nhưng nó hoạt động như một bằng chứng về khái niệm.

private DateCell iniCell=null; 
private DateCell endCell=null; 

private LocalDate iniDate; 
private LocalDate endDate; 
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.MM.uuuu", Locale.ENGLISH);  

@Override 
public void start(Stage primaryStage) { 
    DatePicker datePicker=new DatePicker(); 
    datePicker.setValue(LocalDate.now()); 
    datePicker.setConverter(new StringConverter<LocalDate>() { 

     @Override 
     public String toString(LocalDate object) { 
      if(iniDate!=null && endDate!=null){ 
       return iniDate.format(formatter)+" - "+endDate.format(formatter); 
      } 
      return object.format(formatter); 
     } 

     @Override 
     public LocalDate fromString(String string) { 
      if(string.contains("-")){ 
       try{ 
        iniDate=LocalDate.parse(string.split("-")[0].trim(), formatter); 
        endDate=LocalDate.parse(string.split("-")[1].trim(), formatter); 
       } catch(DateTimeParseException dte){ 
        return LocalDate.parse(string, formatter); 
       } 
       return iniDate; 
      } 
      return LocalDate.parse(string, formatter); 
     } 
    }); 
    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250); 

    primaryStage.setScene(scene); 
    primaryStage.show(); 

    datePicker.showingProperty().addListener((obs,b,b1)->{ 
     if(b1){ 
      DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent(); 

      List<DateCell> cells = content.lookupAll(".day-cell").stream() 
        .filter(ce->!ce.getStyleClass().contains("next-month")) 
        .map(n->(DateCell)n) 
        .collect(Collectors.toList()); 

      // select initial range 
      if(iniDate!=null && endDate!=null){ 
       int ini=iniDate.getDayOfMonth(); 
       int end=endDate.getDayOfMonth(); 
       cells.stream() 
        .forEach(ce->ce.getStyleClass().remove("selected")); 
       cells.stream() 
        .filter(ce->Integer.parseInt(ce.getText())>=ini) 
        .filter(ce->Integer.parseInt(ce.getText())<=end) 
        .forEach(ce->ce.getStyleClass().add("selected")); 
      } 
      iniCell=null; 
      endCell=null; 
      content.setOnMouseDragged(e->{ 
       Node n=e.getPickResult().getIntersectedNode(); 
       DateCell c=null; 
       if(n instanceof DateCell){ 
        c=(DateCell)n; 
       } else if(n instanceof Text){ 
        c=(DateCell)(n.getParent()); 
       } 
       if(c!=null && c.getStyleClass().contains("day-cell") && 
         !c.getStyleClass().contains("next-month")){ 
        if(iniCell==null){ 
         iniCell=c; 
        } 
        endCell=c; 
       } 
       if(iniCell!=null && endCell!=null){ 
        int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
          Integer.parseInt(endCell.getText())); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
      }); 
      content.setOnMouseReleased(e->{ 
       if(iniCell!=null && endCell!=null){ 
        iniDate=LocalDate.of(datePicker.getValue().getYear(), 
             datePicker.getValue().getMonth(), 
             Integer.parseInt(iniCell.getText())); 
        endDate=LocalDate.of(datePicker.getValue().getYear(), 
             datePicker.getValue().getMonth(), 
             Integer.parseInt(endCell.getText())); 
        System.out.println("Selection from "+iniDate+" to "+endDate); 

        datePicker.setValue(iniDate); 
        int ini=iniDate.getDayOfMonth(); 
        int end=endDate.getDayOfMonth(); 
        cells.stream() 
         .forEach(ce->ce.getStyleClass().remove("selected")); 
        cells.stream() 
         .filter(ce->Integer.parseInt(ce.getText())>=ini) 
         .filter(ce->Integer.parseInt(ce.getText())<=end) 
         .forEach(ce->ce.getStyleClass().add("selected")); 
       } 
       endCell=null; 
       iniCell=null;     
      }); 
     } 
    }); 
} 

Range selection and edition

+0

này là phù hợp với những gì tôi đang tìm kiếm. Tôi sẽ đăng lại một giải pháp mạnh mẽ hơn sau này. Tôi cần để có thể chọn ngày vào tháng tới, vì vậy thay vì sử dụng 'getText' để lấy ngày, tôi sẽ sử dụng sự phản chiếu để truy cập 'dayCellDate' trên 'DatePickerContent' để xác định chính xác 'LocalDate'. Ngoài ra, khi lựa chọn được thực hiện, nó sẽ đóng, vì vậy tôi sẽ thêm một 'datePicker.hide();'. Nó sẽ làm việc cả hai phía sau và phía trước để thêm một phương pháp đầu tiên/cuối cùng sẽ cho phép bạn chọn 25 đầu tiên và 11 cuối cùng (trong ví dụ). Tôi đánh giá cao bằng chứng về khái niệm của bạn. – Skjalg

+0

Cảm ơn. Bởi 'tháng tiếp theo' bạn có nghĩa là chọn ngày đã được hiển thị trong cùng một lưới, phải không? Tôi chỉ cần loại bỏ chúng khỏi việc lựa chọn ô để đơn giản. Chúng có kiểu 'next-month', do đó bạn sẽ không cần phản chiếu, chỉ cần đặt cho' datePicker.getValue(). GetMonth() + 1' –

+0

Vâng, ý tôi là chọn các ngày allready được hiển thị. Trong hình trên bạn có thể chọn ví dụ 31. Tháng 12 đến 3 tháng 1. Tôi nhận ra 'tháng tiếp theo' đã có để đơn giản. Tôi nhận ra nhiều người chống lại việc sử dụng sự phản chiếu trong OO, nhưng trong opionion của tôi nó có ý nghĩa hơn ở đây để sử dụng một phương pháp hiện có hơn là để tính toán nó, tuy nhiên simpe. – Skjalg

Các vấn đề liên quan